From 0964d4332c72f6ebb5a3feaa88ffb66f662b41e4 Mon Sep 17 00:00:00 2001 From: Hayden Date: Tue, 18 Aug 2026 23:28:28 +0000 Subject: [PATCH 1/2] fix(security): protect Vercel Edge ecosystem endpoints --- ecosystem-tests/vercel-edge/package.json | 4 +- ecosystem-tests/vercel-edge/src/middleware.ts | 42 +++++++ .../vercel-edge/src/pages/ai-streaming.tsx | 14 ++- .../vercel-edge/src/pages/api/response.ts | 1 + .../vercel-edge/src/pages/api/streaming.ts | 1 + .../src/pages/api/vercel-ai-streaming.ts | 21 +++- .../vercel-edge/src/uploadWebApiTestCases.ts | 18 ++- .../vercel-edge/tests/middleware.test.ts | 103 ++++++++++++++++++ .../tests/streaming-input-limits.test.ts | 63 +++++++++++ .../tests/test-file-cleanup.test.ts | 80 ++++++++++++++ ecosystem-tests/vercel-edge/tests/test.ts | 26 ++++- 11 files changed, 360 insertions(+), 13 deletions(-) create mode 100644 ecosystem-tests/vercel-edge/src/middleware.ts create mode 100644 ecosystem-tests/vercel-edge/tests/middleware.test.ts create mode 100644 ecosystem-tests/vercel-edge/tests/streaming-input-limits.test.ts create mode 100644 ecosystem-tests/vercel-edge/tests/test-file-cleanup.test.ts diff --git a/ecosystem-tests/vercel-edge/package.json b/ecosystem-tests/vercel-edge/package.json index 488fc13e1f..53d69d2168 100644 --- a/ecosystem-tests/vercel-edge/package.json +++ b/ecosystem-tests/vercel-edge/package.json @@ -10,8 +10,8 @@ "edge-runtime": "edge-runtime", "vercel": "vercel", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js", - "test:ci:dev": "start-server-and-test dev http://localhost:3000 test", - "test:ci": "start-server-and-test start http://localhost:3000 test" + "test:ci:dev": "VERCEL_EDGE_TEST_TOKEN=$(node -e \"process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))\") start-server-and-test dev http://localhost:3000 test", + "test:ci": "VERCEL_EDGE_TEST_TOKEN=$(node -e \"process.stdout.write(require('node:crypto').randomBytes(32).toString('hex'))\") start-server-and-test start http://localhost:3000 test" }, "dependencies": { "@ai-sdk/react": "4.0.20", diff --git a/ecosystem-tests/vercel-edge/src/middleware.ts b/ecosystem-tests/vercel-edge/src/middleware.ts new file mode 100644 index 0000000000..ac3a751744 --- /dev/null +++ b/ecosystem-tests/vercel-edge/src/middleware.ts @@ -0,0 +1,42 @@ +import type { NextRequest } from 'next/server'; +import { NextResponse } from 'next/server'; + +const minimumTokenLength = 32; +const maximumRequestBodyBytes = 64 * 1024; + +function matchesAuthorization(actual: string, expected: string): boolean { + let difference = Math.abs(actual.length - expected.length); + + for (let index = 0; index < expected.length; index += 1) { + difference += Number(actual[index] !== expected[index]); + } + + return difference === 0; +} + +export function middleware(request: NextRequest): NextResponse { + const token = process.env.VERCEL_EDGE_TEST_TOKEN; + const authorization = request.headers.get('authorization') ?? ''; + + if ( + !token || + token.length < minimumTokenLength || + !matchesAuthorization(authorization, `Bearer ${token}`) + ) { + return new NextResponse('Unauthorized', { + status: 401, + headers: { 'www-authenticate': 'Bearer' }, + }); + } + + const contentLength = request.headers.get('content-length'); + if (contentLength && Number(contentLength) > maximumRequestBodyBytes) { + return new NextResponse('Payload Too Large', { status: 413 }); + } + + return NextResponse.next(); +} + +export const config = { + matcher: '/api/:path*', +}; diff --git a/ecosystem-tests/vercel-edge/src/pages/ai-streaming.tsx b/ecosystem-tests/vercel-edge/src/pages/ai-streaming.tsx index ba5127fff2..4b6d5ed66f 100644 --- a/ecosystem-tests/vercel-edge/src/pages/ai-streaming.tsx +++ b/ecosystem-tests/vercel-edge/src/pages/ai-streaming.tsx @@ -7,13 +7,14 @@ const transport = new DefaultChatTransport({ api: '/api/vercel-ai-streaming' }); export default function Chat() { const [input, setInput] = useState(''); + const [accessToken, setAccessToken] = useState(''); const { messages, sendMessage } = useChat({ transport }); const handleSubmit = (event: FormEvent) => { event.preventDefault(); - if (!input.trim()) {return;} + if (!input.trim() || !accessToken.trim()) {return;} - void sendMessage({ text: input }); + void sendMessage({ text: input }, { headers: { authorization: `Bearer ${accessToken.trim()}` } }); setInput(''); }; @@ -30,6 +31,15 @@ export default function Chat() { ))}
+