Skip to content

fix: resolve award logo paths before checking /img/awards/ containment (scripts/validate-awards.mjs) - #329

Merged
mrbobbytables merged 1 commit into
mainfrom
sec/awards-logo-containment
Sep 21, 2026
Merged

mrbobbytables merged 1 commit into
mainfrom
sec/awards-logo-containment

Conversation

@hivecommons-hive

Copy link
Copy Markdown
Contributor

Security Fix

Files claimed by this PR: scripts/validate-awards.mjs (award logo
containment check) and tests/validate-awards.test.mjs. No other file is
touched.

scripts/validate-awards.mjs enforced "logo must live under /img/awards/"
by testing the raw value with startsWith('/img/awards/') and then
resolving that same raw value through new URL('../static' + entry.logo, ...),
which normalises .. segments away. The two steps disagreed:

logo = /img/awards/../../../../../etc/hostname
startsWith gate passes: true
resolves to:            /etc/hostname
existsSync:             true      ->  validator exits 0

So the containment invariant never actually held, and the existsSync probe
ran against an arbitrary absolute path on the build runner (validate:awards
runs in deploy-gh-pages.yml and import-architectures.yml). An escaped value
also propagates: scripts/generate-members.mjs pickLogo() copies
awardEntries[0].logo into data/members.json, and
src/components/AwardsTimeline/index.js / src/components/MemberDirectory/index.js
render it as <img src={useBaseUrl(logo)}>.

What changed

  • Resolve the logo path against static/ first, then require the resolved
    path to sit inside static/img/awards/ (with a trailing separator so
    img/awards-other/ cannot pass). The /img/awards/ prefix test is kept as
    well, so the error message and behaviour for ordinary wrong-directory values
    are unchanged.
  • Run existsSync only after containment holds, removing the
    arbitrary-path existence oracle.
  • Reject a non-string truthy logo with a reported error instead of throwing
    TypeError: entry.logo.startsWith is not a function out of the validator.

Falsy/absent logo values are still skipped exactly as before.

Verification

  • npm run test:unit: 58/58 pass (was 55; 3 new cases — .. escape out of
    img/awards/, .. escape out of static/, and a non-string logo).
  • npm run validate:awards against real repo data: Validated 15 award entries,
    exit 0 — no behaviour change for the 10 real /img/awards/*.svg values.
  • npx prettier --check clean on both changed files.

Disjointness

Cut fresh from origin/main @ 00b44df. No other open PR touches
scripts/validate-awards.mjs or tests/validate-awards.test.mjs: #304 changes
scripts/validate-metrics.mjs + tests/validate-metrics-urls.test.mjs, #231
changes tests/helpers.mjs + tests/validate-architecture-assets.test.mjs,
#253 changes tests/members-data.test.mjs, #185 changes
tests/validate-utils.test.mjs.

Closes #327


Filed by sec-check agent (ACMM L4/L5 — hold-gated mode). Hold-gated: human review required.

— hive: agent=sec-check backend=copilot model=claude-opus-5

…/ containment

The award logo gate in scripts/validate-awards.mjs tested the raw value with
startsWith('/img/awards/') and then resolved that same raw value through
new URL(), which normalises '..' away. A value like
/img/awards/../../../../../etc/hostname passed the prefix test and resolved
outside the repository, so the containment invariant never held and the
existsSync probe ran against an arbitrary absolute path on the build runner.

Resolve the path first and check containment against the resolved
static/img/awards/ directory, probing for existence only once containment
holds. Also reject a non-string logo instead of throwing a TypeError out of
startsWith.

Closes #327

Signed-off-by: kubestellar-hive[bot] <kubestellar-hive@hive.kubestellar.io>
@hivecommons-hive

Copy link
Copy Markdown
Contributor Author

Important

Held for human review by the hive's ACMM level gate.

This PR was opened by the "sec-check" agent while Hive policy required a human checkpoint for that agent. Non-outreach agents are held at ACMM L3–L5; the outreach agent is always held because it publishes project-facing communication.

Hive will automatically remove the hold label once current policy no longer requires a level hold for "sec-check". If this is an outreach PR, a human must review it and remove the label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent/security Approved by a Hive merger/owner for auto-merge on green CI hive/hosted-available-lke648397-260827-5n31 Approved by a Hive merger/owner for auto-merge on green CI hold

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[sec-check] validate-awards.mjs logo containment check is defeated by '..' — path escapes static/img/awards/ and existsSync probes the build runner

1 participant