Fix: reject root-relative .. segments and warn per scan

Dot-segment hrefs like /../outside.css could leave the project, and a process-wide warning set hid missing-sheet notices on later detectHtml calls.

AI assistance: implemented with Cursor Grok 4.6.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Abdul Wahab
2026-08-25 19:47:54 +05:00
co-authored by Cursor
parent 2b88aa5231
commit daae1d4117
2 changed files with 21 additions and 4 deletions
@@ -964,13 +964,14 @@ function buildStaticWindow(staticDoc) {
};
}
const warnedMissingStylesheets = new Set();
function resolveLinkedCssPath(fileDir, href) {
const stripped = href.split(/[?#]/)[0];
const rootRelative = stripped.startsWith('/') && !stripped.startsWith('//');
if (!rootRelative) return path.resolve(fileDir, stripped);
const rel = stripped.replace(/^\/+/, '');
// Drop "." and reject ".." so /../outside.css cannot walk out of dir.
const segments = stripped.replace(/^\/+/, '').split(/[/\\]/).filter(p => p && p !== '.');
if (segments.some(p => p === '..')) return path.join(fileDir, segments.filter(p => p !== '..').join(path.sep));
const rel = segments.join(path.sep);
let dir = fileDir;
for (;;) {
const parent = path.dirname(dir);
@@ -990,6 +991,7 @@ function resolveLinkedCssPath(fileDir, href) {
function collectStaticCssText(root, fileDir, profile, filePath, modules) {
const styleTexts = [];
const warnedMissingStylesheets = new Set();
for (const styleEl of modules.selectAll('style', root.children || [])) {
styleTexts.push(modules.domutils.textContent(styleEl));
}
+16 -1
View File
@@ -1358,6 +1358,19 @@ describe('detectHtml — static HTML/CSS engine', () => {
});
});
test('does not follow root-relative .. segments out of the page directory', async () => {
await withStaticFixture({
'project/package.json': '{}',
'project/index.html': `<!DOCTYPE html><html><head>
<link rel="stylesheet" href="/../outside.css">
</head><body><div class="card">Card</div></body></html>`,
'outside.css': '.card { border-left: 5px solid #3b82f6; border-radius: 4px; }',
}, async ({ dir }) => {
const f = await detectHtml(path.join(dir, 'project', 'index.html'));
expect(findingIds(f)).not.toContain('side-tab');
});
});
test('warns when a linked stylesheet cannot be read', async () => {
const writes = [];
const origWrite = process.stderr.write.bind(process.stderr);
@@ -1371,9 +1384,11 @@ describe('detectHtml — static HTML/CSS engine', () => {
<link rel="stylesheet" href="/missing/app.css">
</head><body><div>Page</div></body></html>`,
}, async ({ file, dir }) => {
await detectHtml(file);
await detectHtml(file);
const msg = writes.join('');
expect(msg).toContain('could not read linked stylesheet /missing/app.css');
const hits = msg.split('could not read linked stylesheet /missing/app.css').length - 1;
expect(hits).toBe(2);
expect(msg).toContain(`resolved to ${path.join(dir, 'missing', 'app.css')}`);
});
} finally {