Summary
Ran an adversarial security review against master and verified each finding empirically. Three real supply-chain items independent of the trust model. Two PMP-XSS items relevant under any role-separated future.
A note on the threat model upfront: many XSS vectors require attacker control over PMP, which is currently centrally administered via zerocracy.com (effectively single-admin). Under that model, exploitation requires compromising the central server first — and at that point the server admin has many bigger problems than dashboard XSS. However, if Zerocracy ever adds multi-tenant PMP, role-separated admins, or any other path where a less-privileged actor can write PMP fields, the XSS findings below activate immediately. Treating them as future-proofing rather than skip — listed for awareness even though the current model limits exploitability.
Supply-chain (independent of trust model)
1. action.yml:9 — Docker image pinned to :latest
image: 'docker://yegor256/pages-action:latest'
uses: zerocracy/pages-action@<tag> still pulls whatever is at :latest on Docker Hub. A compromised Docker Hub credential pushes a new :latest, every consumer's next run executes it with ${{ github.token }}.
Note that even an exact tag like :0.7.0 is mutable on Docker Hub — anyone with push access can re-tag the same name to a different image, and consumers won't notice. The robust pin is by content digest:
image: 'docker://yegor256/pages-action@sha256:abc123...'
The SHA-256 digest is the hash of the image manifest, immutable by construction. This mirrors the GitHub Actions convention of pinning uses: to a commit SHA rather than a branch or tag.
2. Gemfile:6 — RubyGems source over plain HTTP
source 'http://rubygems.org'
One-character fix to https. bundle install runs during Docker image build; rubygems.org redirects HTTP→HTTPS in practice, but the declared URL is the initial connection.
3. xsl/vitals.xsl:172-183 — four CDN scripts without SRI
jQuery, jquery.tablesorter, chart.js, chroma-js. The CSS files in the same template already use build-time-computed integrity="sha384-..." hashes (entry.sh handles the hashing); the JS includes don't. Additionally, cdn.jsdelivr.net/npm/chart.js has no version pin — auto-floats to whatever latest resolves to. CDN compromise hits every visitor of every published dashboard.
XSS vectors (relevant under any role-separated PMP future)
4. xsl/bylaws.xsl:21 — bylaws/html rendered with disable-output-escaping="yes"
Pipeline: PMP (award ...) text → Fbe::Award.bylaw.markdown → Redcarpet::Render::HTML (default settings pass inline HTML through) → fact bylaws/html → XSL inserts raw.
Today: only triggered by malicious PMP content (centrally controlled).
Future-proofing: if any non-trusted actor can ever write (award) policy, <script> in markdown text becomes stored XSS on every dashboard. Fix candidates: Redcarpet::Render::HTML.new(escape_html: true, filter_html: true), or run output through Sanitize, or use a markdown renderer that already escapes.
5. xsl/awards.xsl:404-409 — <a href> accepts any URL scheme
Award href field is copied verbatim into the anchor.
Today: only triggered by malicious PMP content.
Future-proofing: if href ever becomes settable from less-privileged sources (e.g. user-submitted links, federated PMP, etc), javascript:alert(1) executes on click. Fix: allowlist https://github.com/... plus any other intended schemes, reject otherwise.
Cosmetic
xsl/badge.xsl $avg declared without as="xs:double". RTF coerced to string then number for comparisons; if $sum / $count ever produces NaN or Infinity, badge color/width silently wrong. No data integrity issue, just visual drift.
What I'd offer
Ready to send small PRs for any of #1–#5. Happy to start with the supply-chain three (Docker digest pin, Gemfile https, JS SRI), they are unconditional wins. The XSS hardening pair (#4 #5) is also straightforward if you want preemptive coverage. Let me know which direction.
Summary
Ran an adversarial security review against master and verified each finding empirically. Three real supply-chain items independent of the trust model. Two PMP-XSS items relevant under any role-separated future.
A note on the threat model upfront: many XSS vectors require attacker control over PMP, which is currently centrally administered via zerocracy.com (effectively single-admin). Under that model, exploitation requires compromising the central server first — and at that point the server admin has many bigger problems than dashboard XSS. However, if Zerocracy ever adds multi-tenant PMP, role-separated admins, or any other path where a less-privileged actor can write PMP fields, the XSS findings below activate immediately. Treating them as future-proofing rather than skip — listed for awareness even though the current model limits exploitability.
Supply-chain (independent of trust model)
1.
action.yml:9— Docker image pinned to:latestuses: zerocracy/pages-action@<tag>still pulls whatever is at:lateston Docker Hub. A compromised Docker Hub credential pushes a new:latest, every consumer's next run executes it with${{ github.token }}.Note that even an exact tag like
:0.7.0is mutable on Docker Hub — anyone with push access can re-tag the same name to a different image, and consumers won't notice. The robust pin is by content digest:The SHA-256 digest is the hash of the image manifest, immutable by construction. This mirrors the GitHub Actions convention of pinning
uses:to a commit SHA rather than a branch or tag.2.
Gemfile:6— RubyGems source over plain HTTPOne-character fix to
https.bundle installruns during Docker image build; rubygems.org redirects HTTP→HTTPS in practice, but the declared URL is the initial connection.3.
xsl/vitals.xsl:172-183— four CDN scripts without SRIjQuery, jquery.tablesorter, chart.js, chroma-js. The CSS files in the same template already use build-time-computed
integrity="sha384-..."hashes (entry.sh handles the hashing); the JS includes don't. Additionally,cdn.jsdelivr.net/npm/chart.jshas no version pin — auto-floats to whateverlatestresolves to. CDN compromise hits every visitor of every published dashboard.XSS vectors (relevant under any role-separated PMP future)
4.
xsl/bylaws.xsl:21—bylaws/htmlrendered withdisable-output-escaping="yes"Pipeline: PMP
(award ...)text →Fbe::Award.bylaw.markdown→Redcarpet::Render::HTML(default settings pass inline HTML through) → factbylaws/html→ XSL inserts raw.Today: only triggered by malicious PMP content (centrally controlled).
Future-proofing: if any non-trusted actor can ever write
(award)policy,<script>in markdown text becomes stored XSS on every dashboard. Fix candidates:Redcarpet::Render::HTML.new(escape_html: true, filter_html: true), or run output throughSanitize, or use a markdown renderer that already escapes.5.
xsl/awards.xsl:404-409—<a href>accepts any URL schemeAward
hreffield is copied verbatim into the anchor.Today: only triggered by malicious PMP content.
Future-proofing: if
hrefever becomes settable from less-privileged sources (e.g. user-submitted links, federated PMP, etc),javascript:alert(1)executes on click. Fix: allowlisthttps://github.com/...plus any other intended schemes, reject otherwise.Cosmetic
xsl/badge.xsl$avgdeclared withoutas="xs:double". RTF coerced to string then number for comparisons; if$sum / $countever produces NaN or Infinity, badge color/width silently wrong. No data integrity issue, just visual drift.What I'd offer
Ready to send small PRs for any of #1–#5. Happy to start with the supply-chain three (Docker digest pin, Gemfile https, JS SRI), they are unconditional wins. The XSS hardening pair (#4 #5) is also straightforward if you want preemptive coverage. Let me know which direction.