]> git.ardis.lu - envpass/commitdiff
Verify fetch metadata request headers on POST ("resource isolation policy")
authorArdis Lu <ardisglu@gmail.com>
Sat, 23 May 2026 06:19:48 +0000 (23:19 -0700)
committerArdis Lu <ardisglu@gmail.com>
Sat, 23 May 2026 06:19:48 +0000 (23:19 -0700)
This is more hardening against cross-origin attacks.

package-lock.json
package.json
src/getPrf.js
test/getPrf.test.js

index efbace50efd28d2273231e537331cc73a97b556d..480a31e0724541c717f7c00a2adaed7ea58b2110 100644 (file)
@@ -18,7 +18,8 @@
       "devDependencies": {
         "@types/node": "^25.8.0",
         "playwright-core": "^1.60.0",
-        "typescript": "^6.0.3"
+        "typescript": "^6.0.3",
+        "undici": "^8.3.0"
       },
       "engines": {
         "node": ">=20"
         "node": ">=14.17"
       }
     },
+    "node_modules/undici": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-8.3.0.tgz",
+      "integrity": "sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=22.19.0"
+      }
+    },
     "node_modules/undici-types": {
       "version": "7.24.6",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
index bf9304b1d478293821ee1d47d13228b43dc5a63f..74cf416b29af1398a6d7447eb2b4be5b088b5ab7 100644 (file)
@@ -65,6 +65,7 @@
   "devDependencies": {
     "@types/node": "^25.8.0",
     "playwright-core": "^1.60.0",
-    "typescript": "^6.0.3"
+    "typescript": "^6.0.3",
+    "undici": "^8.3.0"
   }
 }
index 97e30f1db60b820dd6f3c8df0abf70fa1cb75e58..8dc3c6f2ad142bfcd2cacd6a615448d255f19113 100644 (file)
@@ -114,7 +114,10 @@ export async function getPrf(options = {}) {
     }
     // Verify then use the prf
     else if (request.method === 'POST') {
-      if (request.headers.origin !== `http://localhost:${port}`) {
+      if (request.headers.origin !== `http://localhost:${port}`
+        || request.headers['sec-fetch-site'] !== 'same-origin'
+        || request.headers['sec-fetch-mode'] !== 'cors'
+        || request.headers['sec-fetch-dest'] !== 'empty') {
         response.writeHead(403).end();
         return;
       }
index 229a74bb72c02d27d46664724ee632baebb7fc83..8260ccc002e16fbb934ead9e25855ea6d3b6418d 100644 (file)
@@ -1,6 +1,8 @@
 import { suite, test } from 'node:test';
 import { deepStrictEqual, match, ok, rejects } from 'node:assert/strict';
 
+import { request } from 'undici';
+
 import { WINDOW_EXPIRATION_DURATION } from '#src/getPrf.js';
 import { setupPlaywright, setupServer } from '#test/setup.js';
 
@@ -27,6 +29,9 @@ function makePostRequest(url, options = {}) {
   const mergedHeaders = {
     'Content-Type': 'application/json',
     'Origin': origin,
+    'Sec-Fetch-Site': 'same-origin',
+    'Sec-Fetch-Mode': 'cors',
+    'Sec-Fetch-Dest': 'empty',
     ...headers
   }
   const filteredHeaders = Object.fromEntries(Object.entries(mergedHeaders).filter(([, v]) => v !== null));
@@ -59,6 +64,51 @@ suite('getPrf.js (server)', () => {
     deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Origin': 'null' } }))).status, 403);
     deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Origin': 'https://example.com' } }))).status, 403);
   });
+  test('validates Sec-Fetch-Site header on POST', async (t) => {
+    const { url } = await setupServer(t);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Site': null } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Site': 'cross-site' } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Site': 'same-site' } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Site': 'same-origin' } }))).status, 200);
+  });
+  test('validates Sec-Fetch-Mode header on POST', async (t) => {
+    // undici `fetch` does not allow changing the Sec-Fetch-Mode header, so use the lower-level `request` instead.
+    // See https://github.com/nodejs/undici/issues/1305
+    const { url } = await setupServer(t);
+    const { origin, searchParams } = new URL(url);
+    const challenge = searchParams.get('challenge');
+    const prf = 'deadbeef000000000000000000000000000000000000000000000000cafebabe';
+    const options = {
+      method: 'POST',
+      headers: /** @type {{[key:string]:string}} */({
+        'Content-Type': 'application/json',
+        'Origin': origin,
+        'Sec-Fetch-Site': 'same-origin',
+        'Sec-Fetch-Dest': 'empty'
+      }),
+      body: JSON.stringify({ challenge, prf })
+    }
+    deepStrictEqual((await request(origin, options)).statusCode, 403); // Tests the unset case
+    options.headers['Sec-Fetch-Mode'] = 'navigate';
+    deepStrictEqual((await request(origin, options)).statusCode, 403);
+    options.headers['Sec-Fetch-Mode'] = 'no-cors';
+    deepStrictEqual((await request(origin, options)).statusCode, 403);
+    options.headers['Sec-Fetch-Mode'] = 'same-origin';
+    deepStrictEqual((await request(origin, options)).statusCode, 403);
+    options.headers['Sec-Fetch-Mode'] = 'websocket';
+    deepStrictEqual((await request(origin, options)).statusCode, 403);
+    options.headers['Sec-Fetch-Mode'] = 'cors';
+    deepStrictEqual((await request(origin, options)).statusCode, 200);
+  });
+  test('validates Sec-Fetch-Dest  header on POST', async (t) => {
+    const { url } = await setupServer(t);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': null } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': 'frame' } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': 'iframe' } }))).status, 403);
+    deepStrictEqual((await fetch(makePostRequest(url, { headers: { 'Sec-Fetch-Dest': 'document' } }))).status, 403);
+    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('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.049511 seconds and 4 git commands to generate.