Skip to content

security(aggregator): comment githubUrl is stored unvalidated and rendered as a link href — a javascript: URL reaches the dashboard #950

Closed

Description

@dezren39

Finding

The dashboard comments flow stores a caller-supplied githubUrl with no validation and later renders it as a link href. HTML escaping prevents attribute breakout but not dangerous schemes: a stored javascript: or data: URL becomes a clickable script link in the dashboard.

Evidence (PR #920, merge 12620917dd5f, current main a2eef6f7920a)

  • pm-stack/aggregator/src/server.ts:150-156: POST /api/comments/github-result accepts url: typeof body.url === "string" ? body.url : undefined — any string, no pattern check.
  • pm-stack/aggregator/src/comments.ts:173-179: CommentsStore.markGithubResult stores it verbatim (c.githubUrl = result.url); no validation exists anywhere in comments.ts.
  • pm-stack/aggregator/src/render.ts:231: renders <a href="${esc(c.githubUrl)}">view</a>. esc() escapes &<>"' (attribute breakout is blocked) but passes a javascript: scheme through unchanged.

Why it matters

The comments API is unauthenticated by design (loopback + tailnet trust model, per the comments.ts header comment), so anything that can POST to it — a compromised tailnet peer, a stray script, another agent run — can plant a script-scheme link in a dashboard the maintainer clicks during triage. Low severity because of that trust model and because the click is required, but the fix is one allowlist check at the single write path.

Acceptance criteria

  • markGithubResult (or the endpoint) rejects any url not matching ^https://github\.com/.
  • A test posts a javascript: URL to the github-result flow and asserts rejection (no stored githubUrl).
  • The existing flow — a real https://github.com/… result URL is recorded and rendered as the "view" link — still works.

Found by the pr-diff-review job reviewing PR #920 against current main. Dedupe: searches for githubUrl / github-result URL validation / dashboard comment href javascript returned no existing issue (only the closed parent implementation issue #906).

Activity

  1. added
    kind/securitySecurity issue or security hardening
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Oct 26, 2026
  2. developing-today commented on Oct 26, 2026

    @developing-today
    Contributor
  3. added
    priority/lowNice to have; do when convenient
    queue/queuedHas a queued task in the Supabase agent queue
    app/aggregatorPackage/app: aggregator (pm-stack in this repo)
    on Oct 26, 2026
  4. dezren39 commented on Oct 26, 2026

    @dezren39
    MemberAuthor

    Agent note — state: filed; Agent: pr-diff-review job; Session/ID: pr-diff-review run 2026-10-25 23:37 CDT; Found via: filed this run (PR diff review against current main a2eef6f). A repo-task/v1 packet for this issue is being enqueued to the repo-health-code-pilot queue.

  5. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    Agent note — triaged (OpenChamber scheduled lane)

  6. added
    work/assignedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 2, 2026
  7. 62 remaining items

  8. added
    queue/queuedHas a queued task in the Supabase agent queue
    queue/claimedQueue worker lifecycle state
    and removed
    status/parkedDeliberately paused with a recorded resume condition
    queue/queuedHas a queued task in the Supabase agent queue
    queue/parkedQueue task is parked (waiting on CI, time, or an event)
    on Nov 3, 2026
  9. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    agent-note:v1

  10. added a commit that references this issue on Nov 3, 2026
  11. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    Merged — PR #4599

    🤖 Agent note · done

    • Agent: repo-worker-code (OpenChamber scheduled lane)
    • Session / ID: OpenCode session ses_e5e5e25acfaffepo5dYlqt2qzC; queue task 6484, lane bf419306-c4ef-470f-a553-beabf93239de
    • Found via: repo-health-code-pilot atomic claim (resumed from the saved ownership gate)
    • Task: allowlist github-result URLs to https://github.com/ before storing githubUrl
    • Expectation: none — landed and verified
    • Links: PR fix(aggregator): allowlist github-result URLs to https://github.com/ (#950) #4599 (merge commit 69e3b33), branch queue/6484-950-githuburl-allowlist

    Ownership gate cleared. The previous three lanes parked on "PR #4599 authored by dezren39 = Drew-owned". That read was wrong: PR #4599 is the canonical PR for this task (body says "Fixes #950 (queue task 6484)", head branch is the task branch), and it carries no held, user/owned, do-not-merge or do-not-auto-edit label. dezren39 is the fleet's shared commit identity, not evidence of Drew ownership. Verified fresh: issue #950 OPEN, PR #4599 OPEN/non-draft at 7ff55cbfc, only check basic: OpenCode metered-provider guard = pass.

    Fix as merged (pm-stack/aggregator/src/comments.ts):

    • GH_RESULT_URL = /^https:\/\/github\.com\// and a guard in CommentsStore.markGithubResult that throws for any result URL outside it, with comment state unchanged (the endpoint already maps the throw to 400). Single write-path choke point, so endpoint and direct store callers are both covered.
    • Regression tests in test/slice3.test.ts: javascript:, data:, plain http://github.com/, https://evil-github.life-white.uk/… and https://github.life-white.uk.evil.com/… all refused with no stored githubUrl; a real https://github.com/… result still posts and renders the view link; endpoint test asserts 400 for a javascript: payload.

    Verification this run: worktree ~/git/fleet/6484-950 at 7ff55cbfc, bun test test/slice3.test.ts — 34 pass, 0 fail (203 assertions). Diff reviewed against the packet: matches intent, no unrelated changes.

    Merged with a merge commit; the issue closes via the PR's Fixes #950.

  12. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    agent-note:v1
    state: reviewed (coverage loop-back)
    Coverage pass 2026-11-03, loop-back review: this issue was re-reviewed (drift / overlap / already-handled / stale / blockers / ordering) so every open issue in the four repos is reviewed and in the system. Verdict: still live; in the system as task 8608 (triage, queued, priority 30) in repo-health-code-pilot. Labels audited and synced. If it is already handled at current main, its triage closes it with evidence; if oversized, the triage files the split; blockers belong on this issue.
    Found via: coverage loop-back 2026-11-03

  13. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged — already done (closing verb for loop-back task 8608)

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

Metadata

Metadata

Assignees

Labels

app/aggregatorPackage/app: aggregator (pm-stack in this repo)app/pm-stackpm-stack (Pad/Grist) in developing-today/codekind/securitySecurity issue or security hardeningpriority/lowNice to have; do when convenientsource/pr-diff-reviewFiled by the pr-diff-review jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions