]> git.ardis.lu - envpass/commitdiff
Refuse streaming/chunked requests
authorArdis Lu <ardisglu@gmail.com>
Fri, 17 Jul 2026 07:02:32 +0000 (00:02 -0700)
committerArdis Lu <ardisglu@gmail.com>
Fri, 17 Jul 2026 07:09:24 +0000 (00:09 -0700)
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
test/getPrf.test.js

index 86d1da31f724f43c1de081b037501d805345220d..6663e1c152d5588d586c23a2704f0f39b8ae2eb4 100644 (file)
@@ -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;
index 12911ba8e4e86b6548f2cf4a564e2dfe5c956d5e..656e615e1bac6afa7224a92569d9203bc98138ad 100644 (file)
@@ -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<string,string>} */({
+        '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);
This page took 0.040429 seconds and 4 git commands to generate.