Validate 'next-action' header format in server action requests (#3523)

Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
This commit is contained in:
conico974
2025-08-05 11:31:43 +02:00
committed by GitHub
parent 2f798209bb
commit f72b6a20db
+24 -10
View File
@@ -64,21 +64,35 @@ export async function middleware(request: NextRequest) {
}
async function validateServerActionRequest(request: NextRequest) {
// We need to reject incorrect server actions requests
// We do not do it in cloudflare workers as there is a bug that prevents us from reading the request body.
if (request.headers.has('next-action') && process.env.GITBOOK_RUNTIME !== 'cloudflare') {
// We just test that the json body is parseable
try {
const clonedRequest = request.clone();
await clonedRequest.json();
} catch (e) {
console.warn('Invalid server action request', e);
// If the body is not parseable, we reject the request
// First thing we need to do is validate that the header is in a correct format.
if (request.headers.has('next-action')) {
// A server action id is a 1-byte hex string (2 chars) followed by a 20-byte SHA1 hash (40 chars) = 42 total characters.
// For ref https://github.com/vercel/next.js/blob/db561cb924cbea0f3384e89f251fc443a8aec1ae/crates/next-custom-transforms/src/transforms/server_actions.rs#L266-L268
const regex = /^[a-fA-F0-9]{42}$/;
const match = request.headers.get('next-action')?.match(regex);
if (!match) {
return new Response('Invalid request', {
status: 400,
headers: { 'content-type': 'text/plain' },
});
}
// We need to reject incorrect server actions requests
// We do not do it in cloudflare workers as there is a bug that prevents us from reading the request body.
if (process.env.GITBOOK_RUNTIME !== 'cloudflare') {
// We just test that the json body is parseable
try {
const clonedRequest = request.clone();
await clonedRequest.json();
} catch (e) {
console.warn('Invalid server action request', e);
// If the body is not parseable, we reject the request
return new Response('Invalid request', {
status: 400,
headers: { 'content-type': 'text/plain' },
});
}
}
}
}