Files
cs249r_book/shared/release/release-pill.html
Vijay Janapa Reddi 442dd3693b fix(security): reject non-http(s) hrefs in release-pill
The release pill assigned a data-link attribute straight to a.href, which
CodeQL flags as DOM XSS (js/xss-through-dom). Add a scheme allowlist so only
anchor, relative, and http(s) links pass; anything else such as javascript:
or data: falls back to #release.
2026-07-03 22:04:36 +02:00

134 lines
4.8 KiB
HTML

<!--
MLSysBook release-pill — shared footer version chip for Quarto + static sites.
Renders a small "v0.1.0 · Apr 26, 2026" chip from a deployed
release-manifest.json. Best-effort chrome: silent on any fetch failure
(manifest missing, offline, CORS). Use at any site that emits a manifest
via scripts/version/release.py emit-manifest.
── Standardized placement (all MLSysBook sites) ──
The chip lives in the page-footer, right slot, so it reads as an
intentional part of the footer bar (not dangling below the page). Two
pieces per site:
1. This file loads the style + populator script site-wide:
format:
html:
include-in-header:
- text: |
<meta name="release-manifest" content="/<project-base>/release-manifest.json">
include-after-body:
- file: ../shared/release/release-pill.html
2. Each page-footer places the chip span where it should render:
website:
page-footer:
right:
- text: |
<span class="release-pill" data-release-pill></span>
- icon: github
href: ...
The meta tag tells the script where to find the manifest. The script
populates every span carrying data-release-pill, so the chip can sit in
the footer (canonical), an about page, or anywhere else.
── Manifest contract ──
The manifest is the JSON written by scripts/version/release.py
emit-manifest. Required keys: releaseId, releaseHash. Optional but
recommended: buildDate, project. Anything else is ignored.
── Why runtime fetch instead of build-time templating ──
Build-time templating would be deterministic but requires per-project
Lua/template machinery. Runtime fetch is project-agnostic and works
identically across Quarto, Next.js, and plain HTML. The one fetch per
page-load is cheap and the chip degrades to nothing on failure.
-->
<style>
/* Fixed muted gray on every site — the chip must look identical regardless of
each Quarto theme's accent colors (do NOT inherit --bs-secondary / link color). */
.release-pill {
display: inline-flex;
align-items: baseline;
gap: 0.25em;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
font-size: 0.75rem;
line-height: 1;
color: var(--release-pill-color, #6c757d);
}
.release-pill a,
.release-pill a:visited,
.release-pill-version,
.release-pill-date {
color: var(--release-pill-color, #6c757d);
text-decoration: none;
}
.release-pill a:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.release-pill-date {
opacity: 0.75;
}
</style>
<script>
(function () {
var pills = document.querySelectorAll('[data-release-pill]');
if (!pills.length) return;
// Manifest URL resolution order:
// 1. data-manifest attribute on the pill (per-page override)
// 2. <meta name="release-manifest" content="..."> in <head>
// 3. './release-manifest.json' relative to current page (fallback)
var meta = document.querySelector('meta[name="release-manifest"]');
var metaPath = meta && meta.getAttribute('content');
fetch(metaPath || './release-manifest.json', { cache: 'no-store' })
.then(function (res) { return res.ok ? res.json() : null; })
.then(function (m) {
if (!m || !m.releaseId || !m.releaseHash) return;
var date = m.buildDate
? new Date(m.buildDate).toLocaleDateString('en-US',
{ year: 'numeric', month: 'short', day: 'numeric' })
: '';
var shortHash = String(m.releaseHash).slice(0, 7);
var title =
(m.project ? m.project + ' ' : '') +
'v' + m.releaseId +
(date ? ' · built ' + date : '') +
' · hash ' + shortHash;
pills.forEach(function (el) {
var rawLink = el.getAttribute('data-link') || '#release';
// XSS hardening: allow only anchor, relative, or http(s) links; reject javascript:/data: URLs.
var linkHref = /^(?:#|\/|\.{1,2}\/|https?:\/\/)/i.test(rawLink) ? rawLink : '#release';
var a = document.createElement('a');
a.href = linkHref;
a.title = title;
a.setAttribute('aria-label',
'Release v' + m.releaseId + (date ? ', built ' + date : ''));
var v = document.createElement('span');
v.className = 'release-pill-version';
v.textContent = 'v' + m.releaseId;
a.appendChild(v);
if (date) {
var d = document.createElement('span');
d.className = 'release-pill-date';
d.textContent = ' · ' + date;
a.appendChild(d);
}
el.innerHTML = '';
el.appendChild(a);
});
})
.catch(function () { /* silent: best-effort chrome */ });
})();
</script>