summaryrefslogtreecommitdiff
path: root/docs/_static/js/version-picker.js
diff options
context:
space:
mode:
authorYuriy Andamasov <yuriy@vyos.io>2026-07-28 12:16:46 +0300
committerGitHub <noreply@github.com>2026-07-28 10:16:46 +0100
commit95e9ad86def9b1f33d65a422e1235011e0fb1225 (patch)
tree6113a587f5dbbe4b719ad2bbcd940f29395d89c3 /docs/_static/js/version-picker.js
parenta16c5ef2654ab7bc2a5a54494880543f91c5d89d (diff)
downloadvyos-documentation-95e9ad86def9b1f33d65a422e1235011e0fb1225.tar.gz
vyos-documentation-95e9ad86def9b1f33d65a422e1235011e0fb1225.zip
security: remediate CodeQL code-scanning alerts (#2171)
* security: remediate CodeQL code-scanning alerts (picker XSS sinks, test sanitization, vendored DataTables exclusion) Remediates all 11 open CodeQL alerts on the default branch: - version-picker.js (js/xss-through-dom, alerts 1-3): percent-encode every DOM-derived path component (select.value, parsed location segments) at URL construction time via encodePath()/langUrlFor(), and tighten the parseLocation slug charset to [A-Za-z0-9._-]. No-op on legitimate sphinx slugs — URLs stay byte-identical (asserted by tests). - workers/apex/test/manifest.test.ts (js/incomplete-multi-character- sanitization, alert 6): strip HTML comments from the root.html fixture repeatedly to a fixpoint instead of a single pass. - docs/_static/js/datatables.js (alerts 4,5,7-11): excluded from CodeQL analysis via .github/codeql/codeql-config.yml (new codeql-cfg-path input to the fleet reusable workflow). The file is vendored stock DataTables 1.11.5; the flagged helpers are display/sort normalization, not sanitization boundaries. Excluding keeps the vendored copy byte-identical to upstream instead of hand-patching it. Adds 9 picker tests (hostile-input encoding + slug-charset accept/reject); workers suite 103/103 green. 🤖 Generated by [robots](https://vyos.io) * security: normalize pre-existing percent escapes in encodePath Adversarial-review finding (Codex, medium): location.pathname returns well-formed escapes verbatim, so blind encodeURIComponent double-encoded them (%2E -> %252E), broke the HEAD probe on escaped deep links, and dumped the user at the version root. Each segment is now decoded first (malformed escapes keep the raw segment — no throw), then re-encoded to canonical single encoding. Decoding cannot resurrect dot-segments: the URL parser resolves '.'/'..' and their percent-encoded forms during navigation, so pathname never presents them (verified against the WHATWG parser in Node). workers suite 106/106 (+2 regression tests, mutation-verified). 🤖 Generated by [robots](https://vyos.io) * security: normalize percent escapes per run, not per segment Round-2 adversarial finding (Codex, medium): whole-segment decode meant one malformed escape (a%20b%zz) threw for the segment and double-encoded the valid escapes beside it. encodeSegment now decodes+re-encodes each well-formed %HH run independently; literal spans (including a bare '%') always pass through encodeURIComponent, so taint neutralization holds unconditionally; a run decoding to invalid UTF-8 stays verbatim (already pure %HH text). workers suite 108/108 (+2 discriminating regression tests). 🤖 Generated by [robots](https://vyos.io)
Diffstat (limited to 'docs/_static/js/version-picker.js')
-rw-r--r--docs/_static/js/version-picker.js52
1 files changed, 46 insertions, 6 deletions
diff --git a/docs/_static/js/version-picker.js b/docs/_static/js/version-picker.js
index f13ad60f..84f68fd6 100644
--- a/docs/_static/js/version-picker.js
+++ b/docs/_static/js/version-picker.js
@@ -6,8 +6,10 @@
// Deliberately does not match /pr-<n>/ preview prefixes — previews are single-version,
// so the picker has nothing to switch between and stays hidden there by design.
+ // The (?!\.\.?\/) guard rejects the dot-segments "." and ".." as slugs, which would
+ // otherwise escape the /<lang>/<slug>/ tree once the browser normalizes the URL.
function parseLocation(pathname) {
- var m = pathname.match(/^\/([a-z]{2}(?:_[A-Z]{2})?)\/([^/]+)\/(.*)$/);
+ var m = pathname.match(/^\/([a-z]{2}(?:_[A-Z]{2})?)\/(?!\.\.?\/)([A-Za-z0-9._-]+)\/(.*)$/);
if (!m) return null;
return { lang: m[1], slug: m[2], rest: m[3] };
}
@@ -33,8 +35,38 @@
return null;
}
+ /* Normalize one path segment: each well-formed %HH run is decoded and re-encoded on its
+ * own — per run, not per segment, so one malformed escape cannot double-encode the valid
+ * ones beside it (location.pathname hands escapes back verbatim, and blind re-encoding
+ * would turn %2E into %252E and miss on the HEAD probe). Literal spans, including a bare
+ * '%', always go through encodeURIComponent, so taint neutralization holds unconditionally;
+ * a run decoding to invalid UTF-8 is kept verbatim, being already pure %HH text.
+ * Decoding cannot resurrect a dot-segment: the URL parser resolves '.' / '..' and their
+ * percent-encoded forms during navigation, so pathname never presents them. */
+ function encodeSegment(seg) {
+ var out = '';
+ var re = /(?:%[0-9A-Fa-f]{2})+/g;
+ var last = 0;
+ var m;
+ while ((m = re.exec(seg))) {
+ out += encodeURIComponent(seg.slice(last, m.index));
+ try { out += encodeURIComponent(decodeURIComponent(m[0])); } catch (e) { out += m[0]; }
+ last = m.index + m[0].length;
+ }
+ return out + encodeURIComponent(seg.slice(last));
+ }
+
+ /* Percent-encode a multi-segment path one segment at a time, so the '/' separators
+ * survive. Real docs paths are plain ASCII sphinx slugs (letters/digits/-/_/./html),
+ * where this is a no-op — it exists to keep DOM-derived text (location.pathname,
+ * select.value) from reaching a location.href sink unescaped. */
+ function encodePath(rest) {
+ return rest.split('/').map(function (seg) { return encodeSegment(seg); }).join('/');
+ }
+
function targetUrlFor(loc, targetSlug) {
- return '/' + loc.lang + '/' + targetSlug + '/' + loc.rest;
+ return '/' + encodeURIComponent(loc.lang) + '/' + encodeURIComponent(targetSlug) +
+ '/' + encodePath(loc.rest);
}
/* Full navigation URL for a version switch: same path on the target version
@@ -44,6 +76,13 @@
return targetUrlFor(loc, targetSlug) + (search || '') + (hash || '');
}
+ /* Mirror of targetUrlFor for the language switch: swaps the lang segment while
+ * keeping the current version slug and path. */
+ function langUrlFor(loc, langCode) {
+ return '/' + encodeURIComponent(langCode) + '/' + encodeURIComponent(loc.slug) +
+ '/' + encodePath(loc.rest);
+ }
+
/* ---- DOM layer (no execution at import time) ---- */
function bannerText(b, manifest) {
if (b.kind === 'dev') return 'You are reading the development (rolling) docs.';
@@ -81,7 +120,8 @@
sel.addEventListener('change', function () {
var search = window.location.search, hash = window.location.hash;
var target = navUrlFor(loc, sel.value, search, hash);
- var fallback = '/' + loc.lang + '/' + sel.value + '/' + (search || '') + (hash || '');
+ var fallback = '/' + encodeURIComponent(loc.lang) + '/' + encodeURIComponent(sel.value) +
+ '/' + (search || '') + (hash || '');
fetch(targetUrlFor(loc, sel.value), { method: 'HEAD' })
.then(function (r) {
window.location.href = (r.status === 404) ? fallback : target;
@@ -110,7 +150,7 @@
sel.appendChild(o);
});
sel.addEventListener('change', function () {
- window.location.href = '/' + sel.value + '/' + loc.slug + '/' + loc.rest +
+ window.location.href = langUrlFor(loc, sel.value) +
window.location.search + window.location.hash;
});
anchor.appendChild(sel);
@@ -145,8 +185,8 @@
}
window.VyOSVersionPicker = {
- parseLocation: parseLocation, bannerFor: bannerFor,
- targetUrlFor: targetUrlFor, navUrlFor: navUrlFor, init: init,
+ parseLocation: parseLocation, bannerFor: bannerFor, encodePath: encodePath,
+ targetUrlFor: targetUrlFor, navUrlFor: navUrlFor, langUrlFor: langUrlFor, init: init,
};
if (typeof document !== 'undefined' && document.addEventListener)
document.addEventListener('DOMContentLoaded', init);