From 3b80d0b8e10d56338efcff86b35c49c562755041 Mon Sep 17 00:00:00 2001 From: Blaine Bublitz Date: Wed, 27 Nov 2024 08:43:57 -0700 Subject: [PATCH] fix(nosecone)!: Change return value to Headers --- examples/nodejs-rate-limit/index.js | 5 +- examples/nodejs-rate-limit/package-lock.json | 72 +++++++++----- examples/nodejs-rate-limit/package.json | 3 +- nosecone-next/index.ts | 18 ++-- nosecone-sveltekit/index.ts | 2 +- nosecone/index.ts | 28 +++--- nosecone/test/nosecone.test.ts | 98 +++++++++++--------- 7 files changed, 130 insertions(+), 96 deletions(-) diff --git a/examples/nodejs-rate-limit/index.js b/examples/nodejs-rate-limit/index.js index dbac2568e..e7776b51d 100644 --- a/examples/nodejs-rate-limit/index.js +++ b/examples/nodejs-rate-limit/index.js @@ -1,5 +1,6 @@ import arcjet, { fixedWindow, shield } from "@arcjet/node"; import * as http from "node:http"; +import nosecone from "nosecone"; const aj = arcjet({ // Get your site key from https://app.arcjet.com and set it as an environment @@ -25,6 +26,8 @@ const aj = arcjet({ const server = http.createServer(async function (req, res) { const decision = await aj.protect(req); + res.setHeaders(nosecone()); + if (decision.isDenied()) { res.writeHead(429, { "Content-Type": "application/json" }); res.end(JSON.stringify({ error: "Too Many Requests" })); @@ -34,4 +37,4 @@ const server = http.createServer(async function (req, res) { } }); -server.listen(3000); \ No newline at end of file +server.listen(3000); diff --git a/examples/nodejs-rate-limit/package-lock.json b/examples/nodejs-rate-limit/package-lock.json index 5b51ddd27..22735c0f9 100644 --- a/examples/nodejs-rate-limit/package-lock.json +++ b/examples/nodejs-rate-limit/package-lock.json @@ -1,11 +1,12 @@ { - "name": "nodejs-simple", + "name": "nodejs-rate-limit", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "@arcjet/node": "../../arcjet-node" + "@arcjet/node": "../../arcjet-node", + "nosecone": "../../nosecone" }, "devDependencies": { "@types/node": "^20", @@ -39,22 +40,42 @@ }, "../../arcjet-node": { "name": "@arcjet/node", - "version": "1.0.0-alpha.8", + "version": "1.0.0-alpha.32", "license": "Apache-2.0", "dependencies": { - "@arcjet/ip": "1.0.0-alpha.8", - "@connectrpc/connect-node": "1.3.0", - "arcjet": "1.0.0-alpha.8" + "@arcjet/body": "1.0.0-alpha.32", + "@arcjet/env": "1.0.0-alpha.32", + "@arcjet/headers": "1.0.0-alpha.32", + "@arcjet/ip": "1.0.0-alpha.32", + "@arcjet/logger": "1.0.0-alpha.32", + "@arcjet/protocol": "1.0.0-alpha.32", + "@arcjet/transport": "1.0.0-alpha.32", + "arcjet": "1.0.0-alpha.32" }, "devDependencies": { - "@arcjet/eslint-config": "1.0.0-alpha.8", - "@arcjet/rollup-config": "1.0.0-alpha.8", - "@arcjet/tsconfig": "1.0.0-alpha.8", + "@arcjet/eslint-config": "1.0.0-alpha.32", + "@arcjet/rollup-config": "1.0.0-alpha.32", + "@arcjet/tsconfig": "1.0.0-alpha.32", "@jest/globals": "29.7.0", - "@rollup/wasm-node": "4.12.0", + "@rollup/wasm-node": "4.27.4", "@types/node": "18.18.0", "jest": "29.7.0", - "typescript": "5.3.3" + "typescript": "5.7.2" + }, + "engines": { + "node": ">=18" + } + }, + "../../nosecone": { + "version": "1.0.0-alpha.32", + "license": "Apache-2.0", + "devDependencies": { + "@arcjet/eslint-config": "1.0.0-alpha.32", + "@arcjet/rollup-config": "1.0.0-alpha.32", + "@arcjet/tsconfig": "1.0.0-alpha.32", + "@rollup/wasm-node": "4.27.4", + "@types/node": "18.18.0", + "typescript": "5.7.2" }, "engines": { "node": ">=18" @@ -65,19 +86,25 @@ "link": true }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.17.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.8.tgz", + "integrity": "sha512-ahz2g6/oqbKalW9sPv6L2iRbhLnojxjYWspAqhjvqSWBgGebEJT5GvRmk0QXPj3sbC6rU0GTQjPLQkmR8CObvA==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, + "node_modules/nosecone": { + "resolved": "../../nosecone", + "link": true + }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -87,10 +114,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/examples/nodejs-rate-limit/package.json b/examples/nodejs-rate-limit/package.json index fe60e9bf9..25d25f724 100644 --- a/examples/nodejs-rate-limit/package.json +++ b/examples/nodejs-rate-limit/package.json @@ -5,7 +5,8 @@ "start": "node --env-file .env.local ./index.js" }, "dependencies": { - "@arcjet/node": "../../arcjet-node" + "@arcjet/node": "../../arcjet-node", + "nosecone": "../../nosecone" }, "devDependencies": { "@types/node": "^20", diff --git a/nosecone-next/index.ts b/nosecone-next/index.ts index bb135b703..e8ba8dfb6 100644 --- a/nosecone-next/index.ts +++ b/nosecone-next/index.ts @@ -36,17 +36,13 @@ function nonce() { export function createMiddleware(options: NoseconeOptions = defaults) { return async () => { const headers = nosecone(options); + // Setting this specific header is the way that Next.js implements + // middleware. See: + // https://github.com/vercel/next.js/blob/5c45d58cd058a9683e435fd3a1a9b8fede8376c3/packages/next/src/server/web/spec-extension/response.ts#L148 + // Note: we don't create the `x-middleware-override-headers` header so + // the original headers pass through + headers.set("x-middleware-next", "1"); - return new Response(null, { - headers: { - ...headers, - // Setting this specific header is the way that Next.js implements - // middleware. See: - // https://github.com/vercel/next.js/blob/5c45d58cd058a9683e435fd3a1a9b8fede8376c3/packages/next/src/server/web/spec-extension/response.ts#L148 - // Note: we don't create the `x-middleware-override-headers` header so - // the original headers pass through - "x-middleware-next": "1", - }, - }); + return new Response(null, { headers }); }; } diff --git a/nosecone-sveltekit/index.ts b/nosecone-sveltekit/index.ts index 8d38f7fa9..2853d210e 100644 --- a/nosecone-sveltekit/index.ts +++ b/nosecone-sveltekit/index.ts @@ -29,7 +29,7 @@ export function createHook(options: NoseconeOptions = defaults): Handle { const response = await resolve(event); const headers = nosecone(options); - for (const [headerName, headerValue] of Object.entries(headers)) { + for (const [headerName, headerValue] of headers.entries()) { // Only add headers that aren't already set. For example, SvelteKit will // likely have added `Content-Security-Policy` if configured with `csp` if (!response.headers.has(headerName)) { diff --git a/nosecone/index.ts b/nosecone/index.ts index 3ef84f339..f243158e3 100644 --- a/nosecone/index.ts +++ b/nosecone/index.ts @@ -616,84 +616,84 @@ export default function nosecone({ xPermittedCrossDomainPolicies = defaults.xPermittedCrossDomainPolicies; } - const headers: Record = {}; + const headers = new Headers(); if (contentSecurityPolicy) { const [headerName, headerValue] = createContentSecurityPolicy( contentSecurityPolicy, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (crossOriginEmbedderPolicy) { const [headerName, headerValue] = createCrossOriginEmbedderPolicy( crossOriginEmbedderPolicy, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (crossOriginOpenerPolicy) { const [headerName, headerValue] = createCrossOriginOpenerPolicy( crossOriginOpenerPolicy, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (crossOriginResourcePolicy) { const [headerName, headerValue] = createCrossOriginResourcePolicy( crossOriginResourcePolicy, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (originAgentCluster) { const [headerName, headerValue] = createOriginAgentCluster(); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (referrerPolicy) { const [headerName, headerValue] = createReferrerPolicy(referrerPolicy); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (strictTransportSecurity) { const [headerName, headerValue] = createStrictTransportSecurity( strictTransportSecurity, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xContentTypeOptions) { const [headerName, headerValue] = createContentTypeOptions(); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xDnsPrefetchControl) { const [headerName, headerValue] = createDnsPrefetchControl(xDnsPrefetchControl); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xDownloadOptions) { const [headerName, headerValue] = createDownloadOptions(); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xFrameOptions) { const [headerName, headerValue] = createFrameOptions(xFrameOptions); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xPermittedCrossDomainPolicies) { const [headerName, headerValue] = createPermittedCrossDomainPolicies( xPermittedCrossDomainPolicies, ); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } if (xXssProtection) { const [headerName, headerValue] = createXssProtection(); - headers[headerName] = headerValue; + headers.set(headerName, headerValue); } return headers; diff --git a/nosecone/test/nosecone.test.ts b/nosecone/test/nosecone.test.ts index 65587b5df..239bcaa3d 100644 --- a/nosecone/test/nosecone.test.ts +++ b/nosecone/test/nosecone.test.ts @@ -547,42 +547,46 @@ describe("nosecone", () => { describe("nosecone", () => { it("uses default configuration if no options provided", () => { const headers = nosecone(); - assert.deepStrictEqual(headers, { - "content-security-policy": + assert.deepStrictEqual(Array.from(headers.entries()), [ + [ + "content-security-policy", "base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;", - "cross-origin-embedder-policy": "require-corp", - "cross-origin-opener-policy": "same-origin", - "cross-origin-resource-policy": "same-origin", - "origin-agent-cluster": "?1", - "referrer-policy": "no-referrer", - "strict-transport-security": "max-age=31536000; includeSubDomains", - "x-content-type-options": "nosniff", - "x-dns-prefetch-control": "off", - "x-download-options": "noopen", - "x-frame-options": "SAMEORIGIN", - "x-permitted-cross-domain-policies": "none", - "x-xss-protection": "0", - }); + ], + ["cross-origin-embedder-policy", "require-corp"], + ["cross-origin-opener-policy", "same-origin"], + ["cross-origin-resource-policy", "same-origin"], + ["origin-agent-cluster", "?1"], + ["referrer-policy", "no-referrer"], + ["strict-transport-security", "max-age=31536000; includeSubDomains"], + ["x-content-type-options", "nosniff"], + ["x-dns-prefetch-control", "off"], + ["x-download-options", "noopen"], + ["x-frame-options", "SAMEORIGIN"], + ["x-permitted-cross-domain-policies", "none"], + ["x-xss-protection", "0"], + ]); }); it("uses default configuration if field not provided", () => { const headers = nosecone({}); - assert.deepStrictEqual(headers, { - "content-security-policy": + assert.deepStrictEqual(Array.from(headers.entries()), [ + [ + "content-security-policy", "base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;", - "cross-origin-embedder-policy": "require-corp", - "cross-origin-opener-policy": "same-origin", - "cross-origin-resource-policy": "same-origin", - "origin-agent-cluster": "?1", - "referrer-policy": "no-referrer", - "strict-transport-security": "max-age=31536000; includeSubDomains", - "x-content-type-options": "nosniff", - "x-dns-prefetch-control": "off", - "x-download-options": "noopen", - "x-frame-options": "SAMEORIGIN", - "x-permitted-cross-domain-policies": "none", - "x-xss-protection": "0", - }); + ], + ["cross-origin-embedder-policy", "require-corp"], + ["cross-origin-opener-policy", "same-origin"], + ["cross-origin-resource-policy", "same-origin"], + ["origin-agent-cluster", "?1"], + ["referrer-policy", "no-referrer"], + ["strict-transport-security", "max-age=31536000; includeSubDomains"], + ["x-content-type-options", "nosniff"], + ["x-dns-prefetch-control", "off"], + ["x-download-options", "noopen"], + ["x-frame-options", "SAMEORIGIN"], + ["x-permitted-cross-domain-policies", "none"], + ["x-xss-protection", "0"], + ]); }); it("disables header with explicit false", () => { @@ -601,7 +605,7 @@ describe("nosecone", () => { xPermittedCrossDomainPolicies: false, xXssProtection: false, }); - assert.deepStrictEqual(headers, {}); + assert.deepStrictEqual(Array.from(headers.entries()), []); }); it("enabled default header with explicit true", () => { @@ -620,22 +624,24 @@ describe("nosecone", () => { xPermittedCrossDomainPolicies: true, xXssProtection: true, }); - assert.deepStrictEqual(headers, { - "content-security-policy": + assert.deepStrictEqual(Array.from(headers.entries()), [ + [ + "content-security-policy", "base-uri 'none'; child-src 'none'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; frame-ancestors 'none'; frame-src 'none'; img-src 'self' blob: data:; manifest-src 'self'; media-src 'self'; object-src 'none'; script-src 'self'; style-src 'self'; worker-src 'self'; upgrade-insecure-requests;", - "cross-origin-embedder-policy": "require-corp", - "cross-origin-opener-policy": "same-origin", - "cross-origin-resource-policy": "same-origin", - "origin-agent-cluster": "?1", - "referrer-policy": "no-referrer", - "strict-transport-security": "max-age=31536000; includeSubDomains", - "x-content-type-options": "nosniff", - "x-dns-prefetch-control": "off", - "x-download-options": "noopen", - "x-frame-options": "SAMEORIGIN", - "x-permitted-cross-domain-policies": "none", - "x-xss-protection": "0", - }); + ], + ["cross-origin-embedder-policy", "require-corp"], + ["cross-origin-opener-policy", "same-origin"], + ["cross-origin-resource-policy", "same-origin"], + ["origin-agent-cluster", "?1"], + ["referrer-policy", "no-referrer"], + ["strict-transport-security", "max-age=31536000; includeSubDomains"], + ["x-content-type-options", "nosniff"], + ["x-dns-prefetch-control", "off"], + ["x-download-options", "noopen"], + ["x-frame-options", "SAMEORIGIN"], + ["x-permitted-cross-domain-policies", "none"], + ["x-xss-protection", "0"], + ]); }); }); });