Files
next-ai-draw-io/lib/access-code.ts
T
dayuan.jiang 4731394f32 fix(security): check request sources, regions and endpoints
- Bedrock: a request's AWS region must be a region name. It becomes part
  of the endpoint's host name, so a value such as
  "us-east-1.attacker.example/" sent the server's bearer token or signed
  request to another host.
- MCP preview server: only the preview page itself (Origin equal to the
  Host) or a non-browser client may call it; a page on another localhost
  port could replace the diagram with a plain text POST. History builds
  its thumbnails element by element and shows only SVG data images, so a
  stored value can no longer run script in the preview.
- chat, validate-model, validate-diagram, provider-models and parse-url
  take JSON bodies only, so another website cannot make the user's own
  server (the desktop app, a local install) run models with their keys;
  the desktop app also refuses a foreign Host (DNS rebinding).
- The model list reads at most 2 MB, also through the Gateway SDK, and
  answers only with its own error texts: the URL is the caller's and may
  be an internal address.
- An admin panel provider with its own key and no URL no longer inherits
  the global <P>_BASE_URL, which may be a proxy for another key; OpenAI
  then gets the official endpoint, as its Test. Azure keeps the server's
  resource.
2026-10-05 17:02:06 +09:00

51 lines
1.9 KiB
TypeScript

/**
* Refuse a POST that a page on another website could have sent. A browser
* sends a cross-site POST without asking first (CORS preflight) only with a
* text or form body, so the routes take JSON only. In the desktop app also
* refuse a foreign Host: a site that points its own domain name at
* 127.0.0.1 (DNS rebinding) is same-origin with the local server, but its
* requests carry that domain. A request the server builds itself has no
* Host. Returns the response to send, or null when the request may go on.
*/
export function rejectCrossSite(req: Request): Response | null {
const contentType = req.headers.get("content-type") ?? ""
if (!/^\s*application\/json\b/i.test(contentType)) {
return Response.json(
{ error: "Content-Type must be application/json" },
{ status: 415 },
)
}
const host = req.headers.get("host")
if (
process.env.NEXT_AI_DRAWIO_DESKTOP === "1" &&
host &&
!/^(127\.0\.0\.1|localhost)(:\d+)?$/i.test(host)
) {
return Response.json({ error: "Forbidden" }, { status: 403 })
}
return null
}
/**
* Check the x-access-code header against ACCESS_CODE_LIST.
* Returns a 401 response to send back when the check fails, or null when the
* request may continue (including when no access codes are configured).
*/
export function checkAccessCode(req: Request): Response | null {
const accessCodes =
process.env.ACCESS_CODE_LIST?.split(",")
.map((code) => code.trim())
.filter(Boolean) || []
if (accessCodes.length === 0) return null
const accessCodeHeader = req.headers.get("x-access-code")
if (accessCodeHeader && accessCodes.includes(accessCodeHeader)) return null
return Response.json(
{
error: "Invalid or missing access code. Please configure it in Settings.",
},
{ status: 401 },
)
}