From 42177a0518ce9fbfbf7974dee95243a21ffc9770 Mon Sep 17 00:00:00 2001 From: Ardis Lu Date: Fri, 17 Jul 2026 00:02:32 -0700 Subject: [PATCH] Refuse streaming/chunked requests There is no practical reason the client must stream 32 bytes (should be able to send it in one chunk). This change reduces the attack surface and closes a potential DoS vector. --- src/getPrf.js | 4 ++++ test/getPrf.test.js | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/getPrf.js b/src/getPrf.js index 86d1da3..6663e1c 100644 --- a/src/getPrf.js +++ b/src/getPrf.js @@ -130,6 +130,10 @@ export async function getPrf(options = {}) { response.writeHead(403, { 'Content-Length': 0 }).end(); return; } + if (request.headers['content-length'] === undefined) { // Refuse Transfer-Encoding: chunked + response.writeHead(411, { 'Content-Length': 0 }).end(); + return; + } if (request.headers['content-type'] !== 'application/octet-stream') { response.writeHead(400, { 'Content-Length': 0 }).end(); return; diff --git a/test/getPrf.test.js b/test/getPrf.test.js index 12911ba..656e615 100644 --- a/test/getPrf.test.js +++ b/test/getPrf.test.js @@ -1,5 +1,6 @@ import { suite, test } from 'node:test'; import { deepStrictEqual, match, ok, rejects } from 'node:assert/strict'; +import { Readable } from 'node:stream'; import { request } from 'undici'; @@ -119,6 +120,24 @@ suite('getPrf.js (server)', () => { deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': 'embed' } }))).status, 403); deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': 'empty' } }))).status, 200); }); + test('rejects Transfer-Encoding: chunked on POST', async (t) => { + const { url } = await setupServer(t); + // Using the lower-level `request` instead to force Transfer-Encoding + const { origin, searchParams } = new URL(url); + const options = { + method: 'POST', + headers: /** @type {Record} */({ + 'Content-Type': 'application/octet-stream', + 'Origin': origin, + 'Sec-Fetch-Site': 'same-origin', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Dest': 'empty', + 'Envpass-Challenge': searchParams.get('challenge') + }), + body: Readable.from([Uint8Array.fromHex('deadbeef000000000000000000000000000000000000000000000000cafebabe')]) + } + deepStrictEqual((await request(origin, options)).statusCode, 411); + }); test('validates Content-Type header on POST', async (t) => { const { url } = await setupServer(t); deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Content-Type': null } }))).status, 400); -- 2.47.3