diff --git a/CHANGES.rst b/CHANGES.rst index b47f417e9a1..2bd6aa3e471 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,11 @@ Deprecated Features added -------------- +* #13098: HTML Search: the contents of the search index are sanitised + (reassigned to frozen null-prototype JavaScript objects), to reduce + the risk of unintended access or modification. + Patch by James Addison + Bugs fixed ---------- diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index aaf078d2b91..b2f75e6ab8b 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -219,6 +219,18 @@ const Search = { (document.body.appendChild(document.createElement("script")).src = url), setIndex: (index) => { + const stack = [index]; + const isObject = x => x instanceof Object; + let obj; + while (obj = stack.pop()) { + if (Array.isArray(obj)) { + stack.push(...obj.filter(isObject)); + } else { + stack.push(...Object.values(obj).filter(isObject)); + Object.setPrototypeOf(obj, null); + } + Object.freeze(obj); + } Search._index = index; if (Search._queued_query !== null) { const query = Search._queued_query; @@ -474,7 +486,7 @@ const Search = { const descr = objName + _(", in ") + title; // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) + if (match[2] in Scorer.objPrio) score += Scorer.objPrio[match[2]]; else score += Scorer.objPrioDefault; @@ -520,13 +532,13 @@ const Search = { // add support for partial matches if (word.length > 2) { const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { + if (!(word in terms)) { Object.keys(terms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: terms[term], score: Scorer.partialTerm }); }); } - if (!titleTerms.hasOwnProperty(word)) { + if (!(word in titleTerms)) { Object.keys(titleTerms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); diff --git a/tests/js/searchtools.spec.js b/tests/js/searchtools.spec.js index cfe5fdcf7ed..13da533f6c0 100644 --- a/tests/js/searchtools.spec.js +++ b/tests/js/searchtools.spec.js @@ -25,6 +25,39 @@ describe('Basic html theme search', function() { expect(nextExpected).toEqual(undefined); } + describe('index immutability', function() { + + it('should not be possible to modify index contents', function() { + eval(loadFixture("cpp/searchindex.js")); + + // record some initial state + const initialTitle = Search._index.titles[0]; + const initialTitlesProto = Search._index.titles.__proto__; + const initialTerms = Search._index.terms; + const initialDocNames = [...Search._index.docnames]; + const initialObject = [...Search._index.objects[''][0]]; + + // attempt to mutate the index state + try { Search._index.objects[''][0].pop(); } catch {}; + try { Search._index.objects[''][0].push('extra'); } catch {}; + try { Search._index.docnames.pop(); } catch {}; + try { Search._index.docnames.push('extra'); } catch {}; + Search._index.titles[0] += 'modified'; + Search._index.titles.__proto__ = 'anotherProto'; + Search._index.terms = {'fake': [1, 2, 3]}; + Search._index.__proto__ = 'otherProto'; + + // ensure that none of the modifications were applied + expect(Search._index.__proto__).toBe(undefined); + expect(Search._index.terms).toEqual(initialTerms); + expect(Search._index.titles.__proto__).toEqual(initialTitlesProto); + expect(Search._index.titles[0]).toEqual(initialTitle); + expect(Search._index.docnames).toEqual(initialDocNames); + expect(Search._index.objects[''][0]).toEqual(initialObject); + }); + + }); + describe('terms search', function() { it('should find "C++" when in index', function() {