Files
codebase-memory-mcp/graph-ui/tools/codeql-input-boundaries.test.mjs
Martin Vogel 8c4747666c fix: preserve literal input and static asset boundaries
Escape project names before building hand-test patterns, compare transport
identifiers as literal tokens, preserve boundaries when extracting prose
and XML facts, and require canonical containment for smoke static assets.
Add regression coverage for metacharacters, dollar identifiers, markup
joining, traversal, symlinks, and malformed URL encoding.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-21 23:12:06 +02:00

65 lines
3.1 KiB
JavaScript

import { afterEach, expect, it } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Writable } from 'node:stream';
import { literalPattern } from './literal-pattern.mjs';
import { serveStaticAsset } from './smoke-static-assets.mjs';
const temporary = [];
afterEach(() => { for (const path of temporary.splice(0)) rmSync(path, { recursive: true, force: true }); });
it.each(['a.b', 'a+b', '(a+)+$', '[project]', 'a\\b'])('treats a project name as literal text: %s', name => {
const pattern = literalPattern(name, { start: true, end: true });
expect(pattern.test(name)).toBe(true);
expect(pattern.test(`prefix${name}`)).toBe(false);
expect(pattern.test(`${name}suffix`)).toBe(false);
});
it('does not expand project-name wildcards', () => {
expect(literalPattern('a.b', { start: true, end: true }).test('axb')).toBe(false);
});
function fixture() {
const root = mkdtempSync(join(tmpdir(), 'cbm-static-assets-')); temporary.push(root);
mkdirSync(join(root, 'spike')); mkdirSync(join(root, 'node_modules')); mkdirSync(join(root, 'spike-extra'));
writeFileSync(join(root, 'spike', 'index.html'), 'fixture');
writeFileSync(join(root, 'outside.txt'), 'outside-root');
writeFileSync(join(root, 'spike-extra', 'outside.txt'), 'sibling-root');
return root;
}
function request(root, url) {
return new Promise((resolve, reject) => {
const chunks = []; let status;
const response = new Writable({ write(chunk, encoding, done) { chunks.push(Buffer.from(chunk)); done(); } });
response.writeHead = code => { status = code; };
response.on('error', reject);
response.on('finish', () => resolve({ status, body: Buffer.concat(chunks).toString() }));
try { serveStaticAsset(root, { url }, response); } catch (error) { reject(error); }
});
}
it('serves an allowed static fixture and rejects traversal and sibling prefixes', async () => {
const root = fixture();
expect(await request(root, '/spike/index.html')).toEqual({ status: 200, body: 'fixture' });
expect((await request(root, '/spike/../outside.txt')).status).toBe(404);
expect((await request(root, '/spike/%2e%2e/outside.txt')).status).toBe(404);
expect((await request(root, '/spike-extra/outside.txt')).status).toBe(404);
});
it('rejects a symlink escaping an allowed static root', async () => {
const root = fixture();
symlinkSync(join(root, 'outside.txt'), join(root, 'spike', 'escape.txt'));
expect((await request(root, '/spike/escape.txt')).status).toBe(404);
});
it('returns not found for malformed URL encoding instead of throwing', async () => {
expect((await request(fixture(), '/spike/%zz')).status).toBe(404);
});
it('allows a symlink whose real target stays inside the static root', async () => {
const root = fixture();
symlinkSync(join(root, 'spike', 'index.html'), join(root, 'spike', 'alias.html'));
expect(await request(root, '/spike/alias.html')).toEqual({ status: 200, body: 'fixture' });
});