You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(pm-aggregator): reflected XSS — decoded /item/<subject> interpolated unescaped into the not-found HTML response (Codex P1 on #856, still on main) #2267
Reflected XSS in the PM aggregator: for an unknown /item/<subject> URL, the decoded subject is interpolated unescaped into the HTML not-found response. Codex flagged this as a P1 on PR #856; the PR merged without the finding being acted on, and the defect is confirmed present on main 2026-10-29 (audit spot-check: pm-stack/aggregator/src/server.ts on main still interpolates ${id} raw at line 774, same pattern at 496/603).
Escape the decoded subject in the not-found response
For an unknown /item/<subject> URL, the decoded path is interpolated directly into an HTML response. A link to /item/%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E therefore returns an executable image element rather than displaying the subject as text, allowing reflected JavaScript execution in the aggregator's origin when someone follows the link. Escape id with the renderer's HTML-escaping helper before including it in this response.
Finding: #856 (comment) (filed against pm-stack/aggregator/src/server.ts, PR #856)
Fix direction (as Codex prescribed)
Escape id with the renderer's HTML-escaping helper before including it in this response — and audit the same raw-interpolation pattern at the other sites the audit named (server.ts lines ~496/603) so the fix covers the class, not just the one line.
Source
Codex review-value audit 2026-10-29 (research_notes/codex-review-value-20261029/findings.md §2, best catch #4): 636 Codex findings classified, this one REAL-CATCH, never engaged, PR merged anyway. Verification: defect confirmed present on main 2026-10-29.
Session / ID: cron new-issue-creator-code-b, run 2026-10-31T02:55Z
Found via: filed this run (fresh verification of an already-tracked issue — no duplicate filed)
Task: Re-verified this defect live on current main; adding the evidence here instead of filing a duplicate.
Expectation: Unchanged — this comment only refreshes the verification date/commit.
Links: probe on laptop ~/git/fleet/probe-2026-10-30-laneB-item404/
Fresh verification, main 48f7c867cbf95e9ed51ece83ae0d68d58785ca54 (pm-stack/aggregator/src/server.ts sha256 d68163b8…, identical on sandbox main and the laptop clone; fixture source, loopback):
GET /item/%3Cscript%3Ealert(1)%3C%2Fscript%3E returned HTTP 200 with the payload reflected raw and unescaped in the body:
<h1>Not found</h1><p class="muted">no board item or roadmap row with subject <code><script>alert(1)</script></code></p>
Still live as of 2026-10-31. Sibling observation from the same probe (status codes, not the XSS) is filed separately — it is not part of this issue's fix.
Queue task 3422 started on queue/3422-2267-aggregator-item-escape at d1de7ee. Audit confirms the unknown /item/<subject> 404 interpolates the decoded subject raw. I’m fixing the item response plus the audited raw interpolation siblings in server.ts, and adding aggregator regression coverage for HTML-metacharacter subjects. No edits yet; next update will include test results.
Implementation update: server.ts now applies the aggregator esc helper after path decoding for the unknown /item/<subject> HTML response, and escapes the audited quota-service and program-project 404 interpolations too. Added an HTTP regression using an encoded <img src=x onerror=...>& subject; it asserts escaped inert text and HTTP 404. Initial targeted run passed: bun test test/error-boundary-1211.test.ts (7 tests). Broader item/route tests and typecheck are running next. Changes are limited to server.ts and an aggregator test.
Validation update: full aggregator suite passes (1101/1101 tests) after installing the package's declared dependencies; the focused 3-file run passes 40/40. Live fixture check returned 404 with escaped <img> and <script> subjects, and a known /item/ remains 200. bunx tsc --noEmit still fails on existing unrelated type issues in projects.ts, push.ts typings, and test fixtures; no changed file appears in those errors. Only server.ts and the aggregator regression test are changed. I’m committing and syncing this branch with current main before opening the PR.
Fix is up for review: #4419 (queue/3422-2267-aggregator-item-escape, fix commit fb2998a38).
Escaped the decoded unknown /item/<subject> with the renderer's esc helper; also escaped the quota-service and program-project 404 interpolations in server.ts.
Added the <img onerror> regression test; full local aggregator run is 1101/1101, targeted files 40/40; live fixture probe shows both finding payloads inert at 404 and known /item/ still 200.
tsc --noEmit still reports only pre-existing unrelated errors; git diff --check clean. PR is not merged, awaiting review.
Note: error-boundary.tsnotFoundResponse still reflects the raw page path into its HTML 404 — same class, separate file, left out of scope. Say the word and I'll file it as a follow-up.
Pool: Workhorse — requested opencode-route/workhorse (route id only; pooled route, no single-plan direct seat). Gate: fresh laptop quota read this run ~09:09 CST — meta 5h 8% / weekly 2%, codex weekly 80%, openai.alternate 5h 0% / weekly 0% — Workhorse OPEN; plan quotas only, never PAYG.
Worktree / branch:~/git/fleet/3422-2267-aggregator-item-escape on the laptop, branch queue/3422-2267-aggregator-item-escape (from origin/main d1de7ee)
Verified engaged: yes — 44 messages at first probe, 0 errors; session mid-verification (payloads confirmed rendering as inert escaped text; full suite + typecheck running).
The queue task itself remains queued in the fleet queue (there is no targeted claim/binding verb yet — code#1337 / openchamber#1272); the dispatch is recorded in the task's progress. The owning lane reconciles/harvests from here, and the conductor shepherds the session.
Pickup record: the PR was opened by a dispatcher session (queue task progress origin: openchamber-conductor, stage dispatched) whose binding never landed in the queue. The dispatcher session applied no lifecycle labels and posted no agent note on this issue — recorded as a protocol observation under protocol §9 (GitHub-native binding layer skipped).
Triage verdict: reflected XSS CONFIRMED still present on current main; fix path enqueued.
Evidence (git show main:pm-stack/aggregator/src/server.ts):
:1777 — <h1>Not found</h1><p class="muted">no board item or roadmap row with subject <code>${id}</code></p> — id is the decoded /item/<subject> segment, interpolated raw. A request for /item/%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E therefore returns executable markup in the aggregator origin.
:1190 and :1373 — the same unescaped <code>${...}</code> interpolation in the other two 404 renders (no SaaS service with id / no project with id).
pm-stack/aggregator/src/path-segment.ts:12-18 — decodePathSegment wraps decodeURIComponent only and returns its output verbatim; it offers no HTML escaping, so nothing upstream defuses the payload.
Fix enqueued: queue task 7489 (repo-health-code-pilot, small_fix, priority 10) with the exact scope — escape the path-derived value in all three not-found renders via an HTML-escaping helper, plus a regression test asserting the /item/%3Cimg... shape comes back 404 with the payload only as escaped text. Dedupe key repo-health:v1:fix:developing-today/code:issue:2267. No open PR or branch owns this fix (checked), so no duplicate work risk.
Labels: the stale status/parked + queue/parked are replaced by the in-progress set for the new fix task.
Fix: all three not-found renders in pm-stack/aggregator/src/server.ts now escape the path-derived value with the renderer's existing esc helper (/item/<subject> at :1778, SaaS service id at :1191, program project id at :1374).
Verification:bun test test/error-boundary-1211.test.ts → 7 pass / 0 fail (includes the new regression: /item/%3Cimg%20src%3Dx%20onerror%3D...%3E returns 404 with the payload only as escaped text, no literal <img). No basic:-tier checks were reported for the branch head; the PR was CLEAN/MERGEABLE at merge time and the diff is two files.
Residual:error-boundary.ts notFoundResponse still interpolates the raw page path into its 404 HTML — same reflection class, out of this task's server.ts scope; a follow-up triage task is warranted.
Activity
developing-today commentedon Oct 29, 2026
CODE-1130
dezren39 commentedon Oct 31, 2026
🤖 Agent note · state: filed
~/git/fleet/probe-2026-10-30-laneB-item404/Fresh verification, main
48f7c867cbf95e9ed51ece83ae0d68d58785ca54(pm-stack/aggregator/src/server.tssha256d68163b8…, identical on sandbox main and the laptop clone; fixture source, loopback):GET /item/%3Cscript%3Ealert(1)%3C%2Fscript%3Ereturned HTTP 200 with the payload reflected raw and unescaped in the body:Still live as of 2026-10-31. Sibling observation from the same probe (status codes, not the XSS) is filed separately — it is not part of this issue's fix.
dezren39 commentedon Nov 2, 2026
Queue task 3422 started on
queue/3422-2267-aggregator-item-escapeatd1de7ee. Audit confirms the unknown/item/<subject>404 interpolates the decoded subject raw. I’m fixing the item response plus the audited raw interpolation siblings inserver.ts, and adding aggregator regression coverage for HTML-metacharacter subjects. No edits yet; next update will include test results.dezren39 commentedon Nov 2, 2026
Implementation update:
server.tsnow applies the aggregatoreschelper after path decoding for the unknown/item/<subject>HTML response, and escapes the audited quota-service and program-project 404 interpolations too. Added an HTTP regression using an encoded<img src=x onerror=...>&subject; it asserts escaped inert text and HTTP 404. Initial targeted run passed:bun test test/error-boundary-1211.test.ts(7 tests). Broader item/route tests and typecheck are running next. Changes are limited toserver.tsand an aggregator test.dezren39 commentedon Nov 2, 2026
Validation update: full aggregator suite passes (1101/1101 tests) after installing the package's declared dependencies; the focused 3-file run passes 40/40. Live fixture check returned 404 with escaped
<img>and<script>subjects, and a known/item/remains 200.bunx tsc --noEmitstill fails on existing unrelated type issues inprojects.ts,push.tstypings, and test fixtures; no changed file appears in those errors. Onlyserver.tsand the aggregator regression test are changed. I’m committing and syncing this branch with currentmainbefore opening the PR.dezren39 commentedon Nov 2, 2026
Fix is up for review: #4419 (
queue/3422-2267-aggregator-item-escape, fix commitfb2998a38)./item/<subject>with the renderer'seschelper; also escaped the quota-service and program-project 404 interpolations inserver.ts.<img onerror>regression test; full local aggregator run is 1101/1101, targeted files 40/40; live fixture probe shows both finding payloads inert at 404 and known/item/still 200.tsc --noEmitstill reports only pre-existing unrelated errors;git diff --checkclean. PR is not merged, awaiting review.Note:
error-boundary.tsnotFoundResponsestill reflects the raw page path into its HTML 404 — same class, separate file, left out of scope. Say the word and I'll file it as a follow-up.dezren39 commentedon Nov 2, 2026
🤖 Conductor binding · manual dispatch wave (Drew-ordered queue unblock) 2026-11-02
small_fix— "Fix security(pm-aggregator): reflected XSS — decoded /item/<subject> interpolated unescaped into the not-found HTML response (Codex P1 on #856, still on main) #2267: Reflected XSS in the PM aggregator: the decoded /item/ is interpolated unescaped into the not-found HTML response, so a crafted link executes script in the aggregator origin.") ↔ issue security(pm-aggregator): reflected XSS — decoded /item/<subject> interpolated unescaped into the not-found HTML response (Codex P1 on #856, still on main) #2267ses_e5e632e0f6cffeIerHUTiEmOIS(laptop OpenChamber, agent build)opencode-route/workhorse(route id only; pooled route, no single-plan direct seat). Gate: fresh laptop quota read this run ~09:09 CST — meta 5h 8% / weekly 2%, codex weekly 80%, openai.alternate 5h 0% / weekly 0% — Workhorse OPEN; plan quotas only, never PAYG.~/git/fleet/3422-2267-aggregator-item-escapeon the laptop, branchqueue/3422-2267-aggregator-item-escape(from origin/main d1de7ee)queuedin the fleet queue (there is no targeted claim/binding verb yet — code#1337 / openchamber#1272); the dispatch is recorded in the task's progress. The owning lane reconciles/harvests from here, and the conductor shepherds the session.dezren39 commentedon Nov 2, 2026
🤖 Agent note · bound
queue/3422-…) but the task satqueuedunbound. Task is now parked (reasonpr-bound-shepherd, ~30m) on the PR.openchamber-conductor, stagedispatched) whose binding never landed in the queue. The dispatcher session applied no lifecycle labels and posted no agent note on this issue — recorded as a protocol observation under protocol §9 (GitHub-native binding layer skipped).4 remaining items
dezren39 commentedon Nov 3, 2026
Triage verdict: reflected XSS CONFIRMED still present on current main; fix path enqueued.
Evidence (
git show main:pm-stack/aggregator/src/server.ts)::1777—<h1>Not found</h1><p class="muted">no board item or roadmap row with subject <code>${id}</code></p>—idis the decoded/item/<subject>segment, interpolated raw. A request for/item/%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3Etherefore returns executable markup in the aggregator origin.:1190and:1373— the same unescaped<code>${...}</code>interpolation in the other two 404 renders (no SaaS service with id / no project with id).pm-stack/aggregator/src/path-segment.ts:12-18—decodePathSegmentwrapsdecodeURIComponentonly and returns its output verbatim; it offers no HTML escaping, so nothing upstream defuses the payload.Fix enqueued: queue task 7489 (
repo-health-code-pilot, small_fix, priority 10) with the exact scope — escape the path-derived value in all three not-found renders via an HTML-escaping helper, plus a regression test asserting the/item/%3Cimg...shape comes back 404 with the payload only as escaped text. Dedupe keyrepo-health:v1:fix:developing-today/code:issue:2267. No open PR or branch owns this fix (checked), so no duplicate work risk.Labels: the stale
status/parked+queue/parkedare replaced by the in-progress set for the new fix task.dezren39 commentedon Nov 3, 2026
agent-note:v1 · worker
repo-worker-code· laneaea12c82-c5af-4e5a-9b66-f8e360f87177(openchamber scheduled lane) · transition done · 2026-11-03T12:14Zrepo-health-code-pilot, small_fix, attempt 2) — fix the reflected XSS from the decoded/item/<subject>segment.d1628b4b(merge commit). The PR wasCONFLICTINGwithmainonserver.ts; mergedorigin/main, resolved the conflict keepingmain's multi-line 404 render plusesc(id), pushedbd026c7fa.pm-stack/aggregator/src/server.tsnow escape the path-derived value with the renderer's existingeschelper (/item/<subject>at :1778, SaaS service id at :1191, program project id at :1374).bun test test/error-boundary-1211.test.ts→ 7 pass / 0 fail (includes the new regression:/item/%3Cimg%20src%3Dx%20onerror%3D...%3Ereturns 404 with the payload only as escaped text, no literal<img). Nobasic:-tier checks were reported for the branch head; the PR wasCLEAN/MERGEABLEat merge time and the diff is two files.error-boundary.ts notFoundResponsestill interpolates the raw page path into its 404 HTML — same reflection class, out of this task'sserver.tsscope; a follow-up triage task is warranted.