const assert = require("assert"); const fs = require("fs"); const path = require("path"); const vm = require("vm"); const { pathToRegexp } = require("next/dist/compiled/path-to-regexp"); const { createNextConnectMock, createRes, loadModule, respondErrorMock, respondSuccessMock } = require("../phase21/_shared.cjs"); const rootDirectory = path.join(__dirname, "..", ".."); const toHeaderObject = (headers) => Object.fromEntries(headers.map(({ key, value }) => [key, value])); const getMatchingHeaders = (configuredRules, pathname) => configuredRules .filter(({ source }) => pathToRegexp(source).test(pathname)) .flatMap(({ headers }) => headers); const getCacheControl = (configuredRules, pathname) => getMatchingHeaders(configuredRules, pathname).find( ({ key }) => key.toLowerCase() === "cache-control" )?.value; const applyConfiguredHeaders = (configuredRules, pathname, res) => { for (const { key, value } of getMatchingHeaders( configuredRules, pathname )) { res.setHeader(key, value); } }; const loadAdminNoStore = () => { const adminAuthPath = path.join(rootDirectory, "lib", "admin", "adminAuth.js"); const moduleSource = fs.readFileSync(adminAuthPath, "utf8"); const declaration = moduleSource.match( /export const setAdminAuthNoStore = \(res\) => \{[\s\S]*?\n\};/ ); assert.ok(declaration, "Expected to find setAdminAuthNoStore declaration"); const source = declaration[0] .replace("export const", "const") .concat("\nmodule.exports = { setAdminAuthNoStore };\n"); const context = { module: { exports: {} } }; vm.runInNewContext(source, context, { filename: adminAuthPath }); return context.module.exports.setAdminAuthNoStore; }; const loadMiddleware = () => { const middlewarePath = path.join(rootDirectory, "middleware.js"); const source = fs .readFileSync(middlewarePath, "utf8") .replace( 'import { NextResponse } from "next/server";', 'const { NextResponse } = require("next/server");' ) .replace("export function middleware", "function middleware") .concat("\nmodule.exports = { middleware };\n"); const context = { Buffer, Headers, module: { exports: {} }, process: { env: { NEXTAUTH_URL: "https://example.test", NODE_ENV: "production" } }, require: (moduleName) => { assert.strictEqual(moduleName, "next/server"); return { NextResponse: { next: () => ({ headers: new Headers() }) } }; }, crypto: { randomUUID: () => "00000000-0000-4000-8000-000000000000" } }; vm.runInNewContext(source, context, { filename: middlewarePath }); return context.module.exports.middleware; }; const run = async () => { const nextConfig = require(path.join(rootDirectory, "next.config.js")); const configuredRules = await nextConfig.headers(); const globalRule = configuredRules.find( ({ source }) => source === "/(.*)?" ); assert.ok(globalRule, "Expected the global Next.js security-header rule"); assert.deepStrictEqual(toHeaderObject(globalRule.headers), { "X-DNS-Prefetch-Control": "on", "X-Frame-Options": "SAMEORIGIN", "X-Content-Type-Options": "nosniff", "X-Permitted-Cross-Domain-Policies": "none", "Referrer-Policy": "origin-when-cross-origin", "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload", "X-XSS-Protection": "0" }); const sensitivePaths = [ "/api/admin/auth/session", "/api/email/notify", "/api/file/getprogressobjblob", "/api/file/nested/private/resource", "/api/myportal/get-my-cases", "/api/subscriptions/get-watched-cases", "/api/auth/resolve-locale", "/api/endpoint/createaccount_api", "/api/endpoint/getaccounts_api", "/api/endpoint/getemailaccountcheck_api", "/api/endpoint/getpersonalaccount_api", "/api/endpoint/getportallogin_api", "/api/endpoint/getportalloginproxy_api", "/api/endpoint/getpreferredlanguage_api", "/api/endpoint/updateaccount_api", "/api/endpoint/gethash_api", "/api/endpoint/getmycases_api", "/api/endpoint/getmylpacases_api", "/api/endpoint/getmyrepresentations_api", "/api/endpoint/getmyrepresentationsproxy_api", "/api/endpoint/getawaitingsubmission_api", "/api/endpoint/getawaitingsubmissionproxy_api", "/api/endpoint/getwatchedcases_api", "/api/endpoint/getwatchedcasesproxy_api", "/api/endpoint/createwatchedcases_api", "/api/endpoint/deletewatchedcases_api", "/api/endpoint/deletewatchedcasesproxy_api", "/api/endpoint/deleteawaitingsubmissions_api", "/api/endpoint/getbasicpartsaveddetails_api", "/api/endpoint/getpartsavedappeal_api", "/api/endpoint/getappealid_api", "/api/endpoint/getappealpdfdocuments_api", "/api/endpoint/getportalmoduledetails_api", "/api/endpoint/getportalmoduledetailsproxy_api", "/api/endpoint/patchcase_api", "/api/endpoint/updatecase_api", "/api/endpoint/createcrmtask_api" ]; for (const pathname of sensitivePaths) { assert.strictEqual( getCacheControl(configuredRules, pathname), "no-store", `Expected ${pathname} to receive Cache-Control: no-store` ); } const excludedPaths = [ "/", "/search", "/dnsapplications", "/_next/static/chunks/example.js", "/_next/image", "/api/search/advanced", "/api/cases/get-incident-by-id", "/api/documents/download/example", "/api/notices", "/api/health", "/api/auth/session", "/api/auth/csrf", "/api/endpoint/getbasicsearch_api", "/api/endpoint/getrepresentations_api", "/api/endpoint/getappealtypes_api", "/api/endpoint/getpicklists_api" ]; for (const pathname of excludedPaths) { assert.strictEqual( getCacheControl(configuredRules, pathname), undefined, `Expected ${pathname} to remain outside the no-store rules` ); } const middleware = loadMiddleware(); const response = middleware({ headers: new Headers() }); assert.strictEqual(response.headers.get("X-XSS-Protection"), "0"); assert.strictEqual(response.headers.get("X-Content-Type-Options"), "nosniff"); assert.strictEqual(response.headers.get("X-Frame-Options"), "DENY"); assert.strictEqual( response.headers.get("Referrer-Policy"), "strict-origin-when-cross-origin" ); assert.strictEqual( response.headers.get("Permissions-Policy"), "geolocation=(), camera=(), microphone=(), fullscreen=(self)" ); assert.strictEqual( response.headers.get("Strict-Transport-Security"), "max-age=63072000; includeSubDomains; preload" ); assert.match( response.headers.get("Content-Security-Policy"), /default-src 'self'/ ); const ordinaryResponse = createRes(); applyConfiguredHeaders( configuredRules, "/api/endpoint/getpersonalaccount_api", ordinaryResponse ); respondSuccessMock(ordinaryResponse, { ok: true }); assert.strictEqual( ordinaryResponse.state.headers["cache-control"], "no-store" ); const validationErrorResponse = createRes(); applyConfiguredHeaders( configuredRules, "/api/endpoint/getpersonalaccount_api", validationErrorResponse ); respondErrorMock(validationErrorResponse, { status: 400, code: "CONTACT_ID_REQUIRED", message: "contactid is required" }); assert.strictEqual( validationErrorResponse.state.headers["cache-control"], "no-store" ); const adminResponse = createRes(); applyConfiguredHeaders( configuredRules, "/api/admin/auth/session", adminResponse ); loadAdminNoStore()(adminResponse); assert.strictEqual( adminResponse.state.headers["cache-control"], "no-store, max-age=0" ); assert.strictEqual(adminResponse.state.headers.pragma, "no-cache"); const fileHandler = loadModule("pages/api/file/getrepsblob.js", { nextConnect: createNextConnectMock(), middleware: () => {}, hashAPIPath: () => "&hash=expected", getServerSession: async () => ({ user: { id: "owner-container" } }), authOptions: () => ({}), readAdminSessionCookie: () => null, validateAdminSession: () => false, getRepsBlobs: async () => [{ name: "representation.json" }], downloadAllRepsFiles: async () => [{ id: "representation" }], consoleLogger: () => {}, respondError: respondErrorMock, respondSuccess: respondSuccessMock }); const fileResponse = createRes(); applyConfiguredHeaders( configuredRules, "/api/file/getrepsblob", fileResponse ); await fileHandler.default.handler( { query: { container: "owner-container", hash: "expected" } }, fileResponse ); assert.strictEqual(fileResponse.state.statusCode, 200); assert.strictEqual( fileResponse.state.headers["cache-control"], "no-store" ); const publicCacheDirective = "public, s-maxage=10, stale-while-revalidate=59"; for (const relativePath of [ "pages/dns/index.js", "pages/dnsapplications.js", "pages/myportal/dnsapplications.js" ]) { const pageSource = fs.readFileSync( path.join(rootDirectory, relativePath), "utf8" ); assert.match( pageSource, new RegExp( `res\\.setHeader\\(\\s*["']Cache-Control["']\\s*,\\s*["']${publicCacheDirective.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*\\)` ), `Expected ${relativePath} to preserve its public cache directive` ); } console.log("Security-header regression tests passed (10/10)."); }; if (require.main === module) { run().catch((error) => { console.error(error); process.exit(1); }); } module.exports = run;