mirror of
https://github.com/DayuanJiang/next-ai-draw-io.git
synced 2026-10-12 04:29:51 +08:00
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.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { extractFromHtml } from "@extractus/article-extractor"
|
||||
import { NextResponse } from "next/server"
|
||||
import TurndownService from "turndown"
|
||||
import { checkAccessCode } from "@/lib/access-code"
|
||||
import { checkAccessCode, rejectCrossSite } from "@/lib/access-code"
|
||||
import { readLimitedBody } from "@/lib/read-limited-body"
|
||||
import { isPrivateUrl } from "@/lib/ssrf-protection"
|
||||
|
||||
const MAX_CONTENT_LENGTH = 150000 // Match PDF limit
|
||||
@@ -34,33 +35,9 @@ function detectCharset(
|
||||
}
|
||||
}
|
||||
|
||||
// Read the response body, giving up once it passes MAX_RESPONSE_BYTES so a
|
||||
// huge download can't exhaust server memory. Returns null when too large.
|
||||
async function readLimitedBody(
|
||||
response: Response,
|
||||
): Promise<ArrayBuffer | null> {
|
||||
if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
|
||||
return null
|
||||
}
|
||||
if (!response.body) return new ArrayBuffer(0)
|
||||
|
||||
const reader = response.body.getReader()
|
||||
const chunks: Uint8Array[] = []
|
||||
let total = 0
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
total += value.byteLength
|
||||
if (total > MAX_RESPONSE_BYTES) {
|
||||
await reader.cancel()
|
||||
return null
|
||||
}
|
||||
chunks.push(value)
|
||||
}
|
||||
return new Blob(chunks as BlobPart[]).arrayBuffer()
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const crossSite = rejectCrossSite(req)
|
||||
if (crossSite) return crossSite
|
||||
const accessError = checkAccessCode(req)
|
||||
if (accessError) return accessError
|
||||
|
||||
@@ -128,7 +105,7 @@ export async function POST(req: Request) {
|
||||
)
|
||||
}
|
||||
|
||||
const buffer = await readLimitedBody(response)
|
||||
const buffer = await readLimitedBody(response, MAX_RESPONSE_BYTES)
|
||||
if (!buffer) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user