diff --git a/packages/gitbook/src/middleware.ts b/packages/gitbook/src/middleware.ts index 5af3b631d..6237903a5 100644 --- a/packages/gitbook/src/middleware.ts +++ b/packages/gitbook/src/middleware.ts @@ -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' }, + }); + } + } } }