From 8cc0afb627578d054302c818fc4dadca348b6de2 Mon Sep 17 00:00:00 2001 From: Akash Goenka Date: Tue, 29 Sep 2026 22:31:13 +0530 Subject: [PATCH 1/2] fix: escape quotes in the notebook viewer's esc(); strip nested Java generics (CodeQL) - kb view template: esc() escaped only & < >, but feeds double-quoted attributes (data-dir="${esc(path)}"), so a path or note title containing a quote could close the attribute. Now also escapes " and '. Site copy regenerated. Clears code-scanning alerts #12-20 (js/incomplete-html-attribute-sanitization). - java.ts stripGenerics: one `<[^>]*>` pass turned `Map>` into `Map>`, a name that matches no class. Strip innermost `<...>` repeatedly. Not a security issue (alert #3 is a false positive) but a real resolution bug. - parser.ts extractSfcScripts: accept `` (space before `>`), alert #5. Java probe (scripts/probe-repos.mjs), before -> after: kafka: edges 48881 -> 49034 (+153), java resolved 48376 -> 48529, java ratio 49.7% -> 50.9% jmri: edges 34229 -> 34230 (+1), java resolved 34212 -> 34213, java ratio 37.7% -> 37.8% Co-Authored-By: Claude Sonnet 5.5 --- site-astro/src/notebook-page.html | 4 ++-- src/indexer/extractors/java.ts | 14 +++++++++++--- src/indexer/parser.ts | 5 +++-- src/kb/view-template.ts | 2 +- tests/java-ruby.test.ts | 23 ++++++++++++++++++++++- tests/kb-view.test.ts | 17 +++++++++++++++++ 6 files changed, 56 insertions(+), 9 deletions(-) diff --git a/site-astro/src/notebook-page.html b/site-astro/src/notebook-page.html index ebf9118..26141b7 100644 --- a/site-astro/src/notebook-page.html +++ b/site-astro/src/notebook-page.html @@ -335,7 +335,7 @@ - + ` (space before `>`) is a valid HTML end tag, hence `\s*`. + const re = /]*)?>[\s\S]*?<\/script\s*>/gi; let m: RegExpExecArray | null; while ((m = re.exec(content)) !== null) { const inner = m[0] .replace(/]*)?>/i, '') - .replace(/<\/script>/i, ''); + .replace(/<\/script\s*>/i, ''); blocks.push(inner); } return blocks.length > 0 ? blocks.join('\n') : null; diff --git a/src/kb/view-template.ts b/src/kb/view-template.ts index 4ae565a..61d5138 100644 --- a/src/kb/view-template.ts +++ b/src/kb/view-template.ts @@ -32,4 +32,4 @@ * local file, often offline; a webfont token here would look correct in * the repo and in a screenshot and silently fail for the actual user. */ -export const VIEW_TEMPLATE ="__TITLE__\n\n\n
\n
\n \n
\n Coldstart Notebook\n repo\n
\n
\n
0notes
\n
0links
\n
\n
\n
0%fresh
\n
\n
\n \n \n
\n
\n\n
\n \n\n
\n
Select a note to read.
\n
\n\n
\n \n
\n
Note graph
\n
file note
\n
flow note
\n
Only linked notes shown. Click a node to open it.
\n
\n
\n
\n
\n
\n\n\n\n"; +export const VIEW_TEMPLATE ="__TITLE__\n\n\n
\n
\n \n
\n Coldstart Notebook\n repo\n
\n
\n
0notes
\n
0links
\n
\n
\n
0%fresh
\n
\n
\n \n \n
\n
\n\n
\n \n\n
\n
Select a note to read.
\n
\n\n
\n \n
\n
Note graph
\n
file note
\n
flow note
\n
Only linked notes shown. Click a node to open it.
\n
\n
\n
\n
\n
\n\n\n\n"; diff --git a/tests/java-ruby.test.ts b/tests/java-ruby.test.ts index bba488d..dcb4eb6 100644 --- a/tests/java-ruby.test.ts +++ b/tests/java-ruby.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseFile } from '../src/indexer/parser.js'; -import { parseJavaContent } from '../src/indexer/extractors/java.js'; +import { parseJavaContent, stripGenerics } from '../src/indexer/extractors/java.js'; import { parseRubyContent } from '../src/indexer/extractors/ruby.js'; import { parseKotlinContent } from '../src/indexer/extractors/kotlin.js'; import { ensureParsersReady } from '../src/indexer/extractors/parser-factory.js'; @@ -113,6 +113,27 @@ describe('java-parser — symbol extraction (direct)', () => { expect(cls!.extendsName).toBe('AuthService'); }); + // CodeQL-driven fix: one `<[^>]*>` pass turned `Map>` into + // `Map>` — a name that matches no class, so the edge was silently lost. + it('stripGenerics removes NESTED generic arguments', () => { + expect(stripGenerics('List')).toBe('List'); + expect(stripGenerics('Map>')).toBe('Map'); + expect(stripGenerics('Foo>>')).toBe('Foo'); + expect(stripGenerics('Outer.Inner')).toBe('Outer.Inner'); // qualified names stay qualified + expect(stripGenerics('Plain')).toBe('Plain'); + }); + + it('extends / implements names survive nested generics', () => { + const src = ` + public class Repo extends Base>> implements Comparable, Handler> { + public void run() {} + } + `; + const cls = parseJavaContent(src, AUTH_FILE_ID).symbols.find(s => s.name === 'Repo'); + expect(cls!.extendsName).toBe('Base'); + expect(cls!.implementsNames).toEqual(['Comparable', 'Handler']); + }); + it('extracts method symbols from class body', () => { const src = ` public class AuthService { diff --git a/tests/kb-view.test.ts b/tests/kb-view.test.ts index 22341af..326e128 100644 --- a/tests/kb-view.test.ts +++ b/tests/kb-view.test.ts @@ -87,6 +87,23 @@ describe('kb view — buildViewData', () => { }); }); +describe('kb view — esc (attribute-safe)', () => { + // The template's `esc` feeds double-quoted attributes (data-dir="${esc(path)}"). + // Escaping only & < > let a path or title containing `"` break out of the + // attribute (CodeQL js/incomplete-html-attribute-sanitization). + const esc = new Function(`${VIEW_TEMPLATE.match(/const esc = [^\n]*;/)![0]}; return esc;`)() as (s: unknown) => string; + + it('escapes quotes so a value cannot close an attribute', () => { + const out = esc(`a" onmouseover="x' y`); + expect(out).not.toMatch(/["']/); + expect(out).toBe('a" onmouseover="x' y'); + }); + + it('still escapes the text-node characters', () => { + expect(esc('&')).toBe('<b>&'); + }); +}); + describe('kb view — renderViewHtml', () => { it('injects data and neutralizes so note text cannot break out', () => { const html = renderViewHtml(VIEW_TEMPLATE, { summary: { total: 1 }, notes: [{ body: 'evil text' }] }); From 50c49d3738657cfae745ee22820d7d29b1fac265 Mon Sep 17 00:00:00 2001 From: Akash Goenka Date: Tue, 29 Sep 2026 22:37:09 +0530 Subject: [PATCH 2/2] fix: accept attributes on a script end tag in SFC extraction (CodeQL js/bad-tag-filter) HTML parsers allow whitespace and junk attributes in an end tag, so closes a block. The end-tag regex now matches that too. Co-Authored-By: Claude Sonnet 5.5 --- src/indexer/parser.ts | 7 ++++--- tests/parser.test.ts | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/indexer/parser.ts b/src/indexer/parser.ts index 8ba7bd6..8b5fd50 100644 --- a/src/indexer/parser.ts +++ b/src/indexer/parser.ts @@ -32,13 +32,14 @@ const MAX_FILE_SIZE = 1_000_000; // 1 MB */ function extractSfcScripts(content: string): string | null { const blocks: string[] = []; - // `` (space before `>`) is a valid HTML end tag, hence `\s*`. - const re = /]*)?>[\s\S]*?<\/script\s*>/gi; + // HTML parsers accept whitespace and even junk attributes in an end tag + // (``, ``), hence `(?:\s[^>]*)?`. + const re = /]*)?>[\s\S]*?<\/script(?:\s[^>]*)?>/gi; let m: RegExpExecArray | null; while ((m = re.exec(content)) !== null) { const inner = m[0] .replace(/]*)?>/i, '') - .replace(/<\/script\s*>/i, ''); + .replace(/<\/script(?:\s[^>]*)?>/i, ''); blocks.push(inner); } return blocks.length > 0 ? blocks.join('\n') : null; diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 60e5841..c607ee2 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { join, dirname } from 'node:path'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { parseFile } from '../src/indexer/parser.js'; import { parseTsContent } from '../src/indexer/ts-parser.js'; @@ -1270,3 +1272,19 @@ describe('Groovy / Gradle / Jenkinsfile extractor', () => { expect(result.exports).not.toContain('no-colons'); }); }); + +describe('parser — Vue SFC script extraction', () => { + // HTML parsers accept whitespace and junk attributes in an end tag; a script + // block closed that way must not swallow the rest of the file. + it.each(['', '', ''])('ends a script block at %j', async (close) => { + const dir = mkdtempSync(join(tmpdir(), 'coldstart-sfc-')); + try { + const f = join(dir, 'A.vue'); + writeFileSync(f, `\n\n`); + const r = await parseFile(f, 'vue'); + expect(r!.exports).toEqual(expect.arrayContaining(['first', 'second'])); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +});