mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user