fix: never let a '<' survive title flattening

CodeQL flagged the tag strip in plainTitle: requiring a closing '>' means
a title whose markup never closes keeps its '<script' fragment. The '>'
is optional now, so every '<' starts a strip and none reaches the results
page, and the output is trimmed. Both call sites already HTML-escape the
result; this hardens the flattening itself. Covered by a new test.
This commit is contained in:
only-cli
2026-08-24 10:18:05 -04:00
parent e834309363
commit d56ddb30a0
2 changed files with 15 additions and 2 deletions
+6 -2
View File
@@ -151,8 +151,12 @@ export function searchIndex(index, query) {
// Titles in the index arrive as the HTML of the page's <h1>, markup and all
// (docs.python.org wraps module names in <code> spans), so they are flattened
// to text before they are placed on the results page.
const plainTitle = (t) => String(t).replace(/<[^>]*>/g, '');
// to text before they are placed on the results page. The '>' is optional so
// a tag the title never closes is dropped too, not left standing as '<script'
// for some later layer to trip on: a title is index data from the network,
// and no '<' survives this. A literal less-than in a real title arrives as
// '&lt;', so nothing legitimate is lost.
const plainTitle = (t) => String(t).replace(/<[^>]*>?/g, '').trim();
const escapeHTML = (s) => String(s)
.replaceAll('&', '&amp;').replaceAll('<', '&lt;')
+9
View File
@@ -43,6 +43,15 @@ test('titles are flattened to text before they reach the results page', () => {
assert.equal(found.docs[0].title, 'json - JSON encoder and decoder');
});
test('a tag the title never closes is stripped, not left standing', () => {
// 'json encoder <script src=' has no closing '>', so a strip that requires
// one would hand '<script' onward. No '<' may survive the flattening.
const nested = { ...INDEX, titles: ['json encoder <script src=', ...INDEX.titles.slice(1)] };
const found = searchIndex(nested, 'json');
assert.equal(found.docs[0].title, 'json encoder');
assert.doesNotMatch(resultsToHTML(BASE, 'json', found, nested), /<script/);
});
test('every word must match, and when none can, any-word results say so', () => {
// 'json' hits doc 0, 'socket' hits doc 2, nothing hits both.
const found = searchIndex(INDEX, 'json socket');