From 61092e1c587485e2edd531435cb4292bbc3d0ad5 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Wed, 2 Sep 2026 09:54:41 -0700 Subject: [PATCH] Detect proxy CSP in nested Next apps Recognize proxy files at root or src placement relative to nested Next project markers while continuing to ignore unrelated proxy helpers. AI assistance disclosure: This commit was prepared with Codex under maintainer direction. --- skill/scripts/detect-csp.mjs | 38 +++++++++++++++++++++++++++---- tests/framework-fixtures.test.mjs | 16 +++++++++++-- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/skill/scripts/detect-csp.mjs b/skill/scripts/detect-csp.mjs index 88ae27cb3..1a5664b4d 100644 --- a/skill/scripts/detect-csp.mjs +++ b/skill/scripts/detect-csp.mjs @@ -88,17 +88,45 @@ const NEXT_PROXY_FILES = new Set([ 'proxy.js', 'proxy.mjs', ]); +const NEXT_CONFIG_FILES = [ + 'next.config.js', + 'next.config.mjs', + 'next.config.cjs', + 'next.config.ts', + 'next.config.mts', + 'next.config.cts', +]; const MIDDLEWARE_HINT = /headers\.set\(\s*["']Content-Security-Policy["']/i; const META_TAG_HINT = /http-equiv\s*=\s*["']Content-Security-Policy["']/i; -function isNextRequestHookFile(relPath, base) { +function hasNextProjectMarker(projectRoot) { + if (NEXT_CONFIG_FILES.some(name => fs.existsSync(path.join(projectRoot, name)))) return true; + if (['app', 'pages', 'src/app', 'src/pages'].some(rel => fs.existsSync(path.join(projectRoot, rel)))) return true; + try { + const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')); + return ['dependencies', 'devDependencies', 'peerDependencies'] + .some(group => pkg?.[group] && Object.prototype.hasOwnProperty.call(pkg[group], 'next')); + } catch { + return false; + } +} + +function isNextRequestHookFile(root, absPath, relPath, base) { if (NEXT_MIDDLEWARE_FILES.has(base)) return true; if (!NEXT_PROXY_FILES.has(base)) return false; const normalized = relPath.split(path.sep).join('/').toLowerCase(); // Next.js 16 recognizes proxy at the project root or in the optional src/ - // directory, alongside app/ or pages/. A same-named helper deeper in the - // tree is not the framework request hook. - return normalized === base || normalized === `src/${base}`; + // directory, alongside app/ or pages/. The scan root is commonly a + // monorepo, so also accept that placement relative to a nested directory + // that carries a concrete Next.js project marker. A same-named helper + // elsewhere in the tree is not the framework request hook. + if (normalized === base || normalized === `src/${base}`) return true; + const hookDir = path.dirname(absPath); + const projectRoot = path.basename(hookDir).toLowerCase() === 'src' + ? path.dirname(hookDir) + : hookDir; + if (path.resolve(projectRoot) === path.resolve(root)) return true; + return hasNextProjectMarker(projectRoot); } /** @@ -154,7 +182,7 @@ export function detectCsp(cwd = process.cwd()) { // === detect-only shapes === - if (isNextRequestHookFile(relPath, base) && MIDDLEWARE_HINT.test(body)) { + if (isNextRequestHookFile(cwd, absPath, relPath, base) && MIDDLEWARE_HINT.test(body)) { hits.middleware.push(relPath); } diff --git a/tests/framework-fixtures.test.mjs b/tests/framework-fixtures.test.mjs index 5d4e8825d..ab55a2348 100644 --- a/tests/framework-fixtures.test.mjs +++ b/tests/framework-fixtures.test.mjs @@ -286,20 +286,32 @@ for (const name of listFixtures()) { } describe('detectCsp — Next.js proxy placement', () => { - it('accepts root and src proxy files but ignores same-named nested helpers', () => { + it('accepts proxy files at app roots and src roots but ignores same-named helpers', () => { const source = `export function proxy() { const response = new Response(); response.headers.set('Content-Security-Policy', "script-src 'self'"); return response; }\n`; - for (const [relPath, expectedShape] of [ + for (const [relPath, expectedShape, markers = []] of [ ['proxy.ts', 'middleware'], ['src/proxy.ts', 'middleware'], + ['apps/web/proxy.ts', 'middleware', ['apps/web/app']], + ['apps/docs/src/proxy.ts', 'middleware', ['apps/docs/src/pages']], + ['apps/store/proxy.ts', 'middleware', ['apps/store/package.json']], ['lib/network/proxy.ts', null], + ['apps/web/lib/proxy.ts', null, ['apps/web/app']], ]) { const tmp = mkdtempSync(join(tmpdir(), 'impeccable-proxy-placement-')); try { mkdirSync(dirname(join(tmp, relPath)), { recursive: true }); + for (const marker of markers) { + if (marker.endsWith('package.json')) { + mkdirSync(dirname(join(tmp, marker)), { recursive: true }); + writeFileSync(join(tmp, marker), JSON.stringify({ dependencies: { next: '^16.0.0' } })); + } else { + mkdirSync(join(tmp, marker), { recursive: true }); + } + } writeFileSync(join(tmp, relPath), source); assert.equal(detectCsp(tmp).shape, expectedShape, relPath); } finally {