Merge pull request #2757 from DeusData/codex/codeql-main-alerts

fix: preserve literal input and static asset boundaries
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-25 18:39:47 +01:00
commit 869fc2b54c
15 files changed
+191 -23

No files matched your search

@@ -343,3 +343,9 @@ describe('Spring service connection evidence', () => {
expect(links(result)).toEqual([]);
});
});
it.each(['$endpoint', 'endpoint$'])('tracks a literal identifier through a transport wrapper: %s', parameter => {
const compose = 'services:\n web:\n build: ./web\n api:\n image: example/api\n';
const source = `function load(${parameter}) { return fetch(${parameter}); }\nload("http://api:8080/items");`;
expect(links(buildContainerTopology([project(compose, { 'web/client.js': source })]))).toEqual(['web>api:http']);
});
@@ -300,7 +300,12 @@ function networkCall(name: string): boolean {
|| /^(?:requests|http|https|axios|urllib\.request|urllib3|grpc|redis|psycopg2?|pymysql|mysql|amqp|amqplib|net|socket|ConnectionMultiplexer|Dns)\./i.test(name)
&& /(?:get|post|put|patch|delete|request|urlopen|dial|newclient|connect|connectasync|createclient|createconnection|insecure_channel|secure_channel|gethostentry(?:async)?)$/i.test(name);
}
const wordIn = (code: string, name: string): boolean => new RegExp(`\\b${name.replace(/[$]/g, '\\$&')}\\b`).test(code);
function wordIn(code: string, name: string): boolean {
for (const [identifier] of code.matchAll(/[A-Za-z_$][\w$]*/g)) {
if (identifier === name) return true;
}
return false;
}
interface Wrapper { parameter: number; hint: string }
/** Bounded local argument propagation to known transport APIs, not a function-name dictionary. */
function connectionWrappers(code: string, calls: Call[]): Map<string, Wrapper> {
@@ -64,3 +64,10 @@ it('follows only actual call edges across components and retains their evidence'
expect(paths[0].edges[0]).toBe(map.evidence[0]);
expect(entryRoutes(graph.nodes[2], map.evidence)).toEqual([]);
});
it('keeps token boundaries when dropping markup from repository prose', () => {
const quote = sourceQuote('README.md', 'This repository provides useful source browsing and <<script>script> examples for developers.');
expect(quote?.text).not.toContain('<script>');
expect(sourceQuote('README.md', 'This repository provides useful source<b>browsing</b> for developers.')?.text)
.toBe('This repository provides useful source browsing for developers.');
});
@@ -41,7 +41,7 @@ export function sourceCandidates(area: RepositoryArea): string[] {
}
function plainMarkdown(text: string): string {
return text.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*_`]/g, '').replace(/<[^>]*>/g, '').trim();
return text.replace(/!\[[^\]]*\]\([^)]*\)/g, '').replace(/\[([^\]]+)\]\([^)]*\)/g, '$1').replace(/[*_`]/g, '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
}
/** Quote only repository-authored prose, never infer a responsibility from an identifier. */
@@ -102,3 +102,10 @@ describe('facts read from a configuration or text file (K12)', () => {
expect(readerFacts(reader('manage.py', 'import os\n'))).toEqual([]);
});
});
// Removing a comment must not create a root tag that was absent from the input.
it('does not join XML markup across removed declarations or comments', () => {
expect(fileFactLines('pom.xml', '<<!-- ignored -->fake/><project/>')).toContain('Root element: `<project>`.');
expect(fileFactLines('pom.xml', '<<?meta?>fake/><project/>')).toContain('Root element: `<project>`.');
expect(fileFactLines('pom.xml', '<!-- <hidden/> --><project/>')).toContain('Root element: `<project>`.');
});
+6 -2
View File
@@ -130,8 +130,12 @@ function patternFacts(text: string): string[] {
}
function xmlFacts(text: string): string[] {
const root = /<([A-Za-z_][\w:.-]*)[\s>/]/.exec(text.replace(/<\?[\s\S]*?\?>|<!--[\s\S]*?-->|<![^>]*>/g, ''));
return root ? [words.root(quote(`<${root[1]}>`))] : [];
// Skip markup in the original input: deleting it could join fragments into a new tag.
const tokens = /<\?[\s\S]*?(?:\?>|$)|<!--[\s\S]*?(?:-->|$)|<![^>]*>|<([A-Za-z_][\w:.-]*)[\s>/]/g;
for (const match of text.matchAll(tokens)) {
if (match[1]) return [words.root(quote(`<${match[1]}>`))];
}
return [];
}
/** The structure of the text, by the kind its name gives. */
@@ -0,0 +1,64 @@
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' });
});
@@ -33,6 +33,7 @@
*/
import { chromium } from 'playwright';
import { architectureNodeSelector, projectNamePattern } from './handtest-locators.mjs';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import { inflateSync } from 'node:zlib';
@@ -446,7 +447,7 @@ async function hoverSweep(page) {
const hid = [];
let shown = false;
for (const id of ids.slice(0, 14)) {
const chip = page.locator(`button.architecture-node-label[data-node-id="${id.replace(/"/g, '\\"')}"]`).first();
const chip = page.locator(architectureNodeSelector(id)).first();
if (!(await chip.isVisible().catch(() => false))) continue;
await chip.hover({ timeout: 3000 }).catch(() => {});
await wait(450);
@@ -498,7 +499,7 @@ async function k21(page) {
async function switchProject(page, name) {
await page.locator('.atlas-shell details summary', { hasText: PROJECT }).first().click();
const entry = page.locator('.atlas-project-results button', { has: page.locator('.atlas-project-result-name', { hasText: new RegExp(`^${name}$`) }) }).first();
const entry = page.locator('.atlas-project-results button', { has: page.locator('.atlas-project-result-name', { hasText: projectNamePattern(name) }) }).first();
await entry.waitFor({ timeout: 20000 });
await entry.click();
}
+3 -2
View File
@@ -22,6 +22,7 @@
* access to Hugging Face), to switch the project while the download runs; the cache is full again after it.
*/
import { literalPattern } from './literal-pattern.mjs';
import { chromium } from 'playwright';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
@@ -843,7 +844,7 @@ const activeWorkspace = (page) => page.locator('[data-workspace-tab][aria-select
const sameText = (left, right) => left.replace(/\d[\d,.:]*/g, '#').replace(/\s+/g, ' ') === right.replace(/\d[\d,.:]*/g, '#').replace(/\s+/g, ' ');
async function switchTo(page, project) {
await page.locator('.atlas-project-switcher summary').first().click();
await page.locator('.atlas-project-picker button', { has: page.locator('.atlas-project-result-name', { hasText: new RegExp(`^${project}`) }) }).first().click();
await page.locator('.atlas-project-picker button', { has: page.locator('.atlas-project-result-name', { hasText: literalPattern(project, { start: true }) }) }).first().click();
}
const pageState = (page) => page.evaluate(() => ({ id: window.__probePage, origin: performance.timeOrigin, workers: window.__probeModelWorkers }));
@@ -1064,7 +1065,7 @@ async function k24(page) {
const downloads = modelRequests.length - requestsBefore;
await save('K24', 'legs.json', legs);
const legOk = (leg) => leg.samePage && leg.workspace.before === leg.workspace.after && leg.modelWorkersCreated === 0 && leg.prepares === 0 && leg.lamp.every((frame) => /Agent active/.test(frame.label ?? ''))
&& new RegExp(leg.to).test(leg.project ?? '') && leg.requestsNamingOldProject.length === 0 && leg.views.length === K24_VIEWS.length && leg.views.every((view) => !view.stale && !view.unexpected.length) && leg.galaxySameAsFresh
&& literalPattern(leg.to).test(leg.project ?? '') && leg.requestsNamingOldProject.length === 0 && leg.views.length === K24_VIEWS.length && leg.views.every((view) => !view.stale && !view.unexpected.length) && leg.galaxySameAsFresh
&& (leg.to !== CONTROL || leg.chatHasOldTurn === false) && (leg.to !== PROJECT || leg.how !== 'switcher' || leg.chatKeptOwnTurn === true);
check('K24', 'A project switch stays in the page: the model stays loaded (no reload, no new worker, lamp active), every workspace shows only the new project and the Galaxy its graph, no request names the old one; Back/Forward the same, all views checked',
loaded && downloads === 0 && legs.every(legOk),
+2 -1
View File
@@ -31,6 +31,7 @@
* wird im Browser beantwortet und navigator.sendBeacon bleibt in der Seite.
*/
import { literalPattern } from './literal-pattern.mjs';
import { chromium } from 'playwright';
import { mkdir, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
@@ -460,7 +461,7 @@ try {
// Projektwechsel nach cbm: frischer Verlauf
await page.locator('.atlas-shell details summary', { hasText: PROJECT }).first().click();
const entry = page.locator('.atlas-project-results button', { has: page.locator('.atlas-project-result-name', { hasText: new RegExp(`^${CONTROL}$`) }) }).first();
const entry = page.locator('.atlas-project-results button', { has: page.locator('.atlas-project-result-name', { hasText: literalPattern(CONTROL, { start: true, end: true }) }) }).first();
await entry.waitFor({ timeout: 20000 });
await entry.click();
const fresh = { position: '1/1', back: { disabled: true, title: 'Nothing to go back to yet' }, forward: { disabled: true, title: 'Nothing to go forward to' }, recent: [] };
+14
View File
@@ -0,0 +1,14 @@
import { literalPattern } from './literal-pattern.mjs';
/** Match one architecture chip by its graph node ID. */
export function architectureNodeSelector(id) {
// CSS strings need escapes for backslashes, quotes, and control characters.
// Terminate each hex escape so the next character cannot extend its code point.
const escaped = id.replace(/[\x00-\x1f\x7f"\\]/g, char => `\\${char.codePointAt(0).toString(16)} `);
return `button.architecture-node-label[data-node-id="${escaped}"]`;
}
/** Match a complete project name in the project chooser. */
export function projectNamePattern(name) {
return literalPattern(name, { start: true, end: true });
}
+24
View File
@@ -0,0 +1,24 @@
import { expect, it } from 'vitest';
import { JSDOM } from 'jsdom';
import { architectureNodeSelector, projectNamePattern } from './handtest-locators.mjs';
it.each(['node', 'node"name', 'node\\name', 'node\\"name', 'node\nname', 'node\rname', 'node\fname', 'node], button'])('selects only the literal graph node ID: %j', id => {
const { document } = new JSDOM('<button class="architecture-node-label" data-node-id="other"></button>').window;
const target = document.createElement('button');
target.className = 'architecture-node-label';
target.setAttribute('data-node-id', id);
document.body.append(target);
expect(document.querySelectorAll(architectureNodeSelector(id))).toHaveLength(1);
expect(document.querySelector(architectureNodeSelector(id))).toBe(target);
});
it.each(['a.b', 'a+b', '(project)', '[project]', 'a\\b', '*', '(a+)+$'])('matches a complete literal project name: %j', name => {
const pattern = projectNamePattern(name);
expect(pattern.test(name)).toBe(true);
expect(pattern.test(`prefix${name}`)).toBe(false);
expect(pattern.test(`${name}suffix`)).toBe(false);
});
it('does not treat a project-name dot as a wildcard', () => {
expect(projectNamePattern('a.b').test('axb')).toBe(false);
});
+5
View File
@@ -0,0 +1,5 @@
/** Build a pattern for literal text with optional start/end anchoring. */
export function literalPattern(value, { start = false, end = false } = {}) {
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
return new RegExp(`${start ? '^' : ''}${escaped}${end ? '$' : ''}`);
}
+4 -14
View File
@@ -31,9 +31,10 @@ import { createServer } from 'node:http';
import { request as httpRequest } from 'node:http';
import net from 'node:net';
import { mkdtemp, mkdir, writeFile, readFile, stat, rm } from 'node:fs/promises';
import { existsSync, createReadStream } from 'node:fs';
import { existsSync } from 'node:fs';
import { serveStaticAsset } from './smoke-static-assets.mjs';
import { tmpdir } from 'node:os';
import { join, dirname, resolve, extname, normalize } from 'node:path';
import { join, dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
@@ -251,8 +252,6 @@ const MIME = {
};
function startStaticProxy(port, cPort) {
const allowedRoots = [join(ROOT, 'spike'), join(ROOT, 'node_modules')];
const server = createServer((req, res) => {
if (req.method === 'POST' && req.url.split('?')[0] === '/rpc') {
const chunks = [];
@@ -284,16 +283,7 @@ function startStaticProxy(port, cPort) {
return;
}
const urlPath = decodeURIComponent(req.url.split('?')[0]);
const candidate = resolve(join(ROOT, normalize(urlPath)));
const allowed = allowedRoots.some((root) => candidate === root || candidate.startsWith(root + '/'));
if (!allowed || !existsSync(candidate)) {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('not found');
return;
}
res.writeHead(200, { 'Content-Type': MIME[extname(candidate)] ?? 'application/octet-stream' });
createReadStream(candidate).pipe(res);
serveStaticAsset(ROOT, req, res, MIME);
});
return new Promise((resolveServer, rejectServer) => {
+39
View File
@@ -0,0 +1,39 @@
import { createReadStream, realpathSync, statSync } from 'node:fs';
import { resolve, extname, sep } from 'node:path';
/** Serve the smoke fixture and installed assets only, including after resolving symlinks. */
export function serveStaticAsset(root, req, res, mime = {}) {
const notFound = () => {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('not found');
};
let candidate;
try {
const urlPath = decodeURIComponent((req.url ?? '/').split('?')[0]);
candidate = resolve(root, `.${urlPath}`);
} catch {
notFound();
return;
}
for (const directory of ['spike', 'node_modules']) {
const allowedRoot = resolve(root, directory);
if (!candidate.startsWith(allowedRoot + sep)) continue;
try {
const realRoot = realpathSync(allowedRoot);
const realCandidate = realpathSync(candidate);
if (!realCandidate.startsWith(realRoot + sep)) continue;
if (!statSync(realCandidate).isFile()) break;
const stream = createReadStream(realCandidate);
stream.once('error', () => {
if (!res.headersSent) notFound();
else res.destroy();
});
res.writeHead(200, { 'Content-Type': mime[extname(realCandidate)] ?? 'application/octet-stream' });
stream.pipe(res);
return;
} catch {
break;
}
}
notFound();
}