PR #3367 · security

Nine code-scanning alerts, three root causes

The badge said nine. Two were real defects and are fixed here. Three are one written decision counted three times. The remaining four are the same three findings counted twice, because plugins/ is build output generated from src/ and the scanner reads both.

The collapse

Nine badge entries on the left, what they actually are on the right.

Reported
PinnedDependenciesID
js/incomplete-html-attribute-sanitization
zizmor/artipacked
Actual
1attribute escaping — fixed
1credential persistence — fixed
1Dockerfile tag pin — deliberate
rest: src/ vs plugins/ double count

The three findings

Open each for the reasoning. Two shipped, one deliberately did not.

fixed Escaping helper covered angle brackets but not quotes plan-viz.html:173

esc() escaped &, < and > and nothing else, while its output is interpolated into attributes:

class="chip ${esc(h.p)}"      line 196
data-f="${esc(h.f)}"          line 200

A value containing a double quote closes the attribute and opens a new one. esc() now escapes both quote forms.

Blast radius is genuinely small — a static playground with hardcoded data, no user input path. It is fixed anyway because an escaping helper is exactly the kind of thing that gets copied into somewhere that does take input.

fixed Checkout left the token in .git/config for every later step import-symbol-watch.yml

The checkout was already SHA-pinned, which was right, but left persist-credentials at its default of true. That keeps GITHUB_TOKEN in .git/config for the rest of the job.

The job declares contents: read, uses no secrets, and runs no git push or gh api, so the credential has nothing to do after checkout.

This was the outlier, not a new convention: 40 other workflows already set persist-credentials: false. actionlint rc=0.

left open Dockerfile pins by tag, and says so in writing devops-deployment/scripts/Dockerfile

Three alerts land on lines 9, 19 and 33. The file already carries a reasoned position at lines 3–6:

pin by tag here, not by digest. A digest pin freezes the base layer forever, so the image can never pick up an upstream security patch. If you do pin a digest for reproducibility, treat it as a dependency and refresh it deliberately (renovate/dependabot or a scheduled bump), never set-and-forget.

A scanner cannot see that argument, so it reports the absence of a digest as a finding. Operator decision, 2026-08-09: leave as-is and do not dismiss. The three alerts stay open as a standing known-accepted item rather than being silenced, so the position stays visible and re-arguable instead of disappearing into a dismissal reason.

The escape hatch the comment itself names — digest plus a scheduled refresh — remains available if the trade-off is ever revisited.

What the badge count is worth

The generated-output double count is the part worth remembering.

plugins/ is build output assembled from src/ by npm run build. Both trees are committed, so a single defect in a skill file is scanned and reported twice. Any alert total that includes generated paths overstates by roughly the size of the duplicated surface.

That is not an argument for ignoring the badge, it is an argument for collapsing it to root causes before deciding anything — which is what the diagram above does. Nine entries, three decisions, two commits.

Re-run this triage on the current alert set.