Skip to content

fix(handlers): XSS in titles/headlines + URL-encode slugs + RM_BIND fail-fast#14

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/codex-followup-w1-w4
Jun 21, 2026
Merged

fix(handlers): XSS in titles/headlines + URL-encode slugs + RM_BIND fail-fast#14
AdaWorldAPI merged 1 commit into
mainfrom
claude/codex-followup-w1-w4

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Closes codex items on my Phase 0/1 PRs

Severity PR What Fix
P1 #10 XSS via raw subject in <title> + headline anchor common::html_escape + escape at handler call sites
P2 #12 XSS via raw spent_on in headline same html_escape pattern
P2 #13 Role names with /#? break URL parse common::encode_path_segment (RFC-3986)
P1 #7 Malformed RM_BIND silently falls back to default Distinguish unset vs malformed; malformed → io::Error
#9 Auth routes not wired Already fixed in #10's build_router_with
#8 Lowercase vs PascalCase OGAR table names Schema-divergence, dedicated PR

XSS — what was broken

wrap_in_doc(title, body) interpolated raw title straight into the document's <title> element. A subject like </title><script>alert(1)</script> reaches it verbatim from find_issue(...)?.subject.

render_detail(..., headline_html, ...) is contractually pre-rendered HTML (|safe in the askama template). W1–W4 handlers all composed it like:

let headline = format!("<a href="{}" class="primary-link">{}</a>", href, &user_data);

Stored XSS. Both paths fixed via the new html_escape.

URL encoding — what was broken

/roles/<role_name> with R&D becomes /roles/R&D — the & is a parameter delimiter for some servers; # would terminate at a fragment; / would split into another path segment. Now routed through encode_path_segment:

list_percent_encodes_role_names_with_reserved_chars: Q/A → /roles/Q%2FA
                                                   R&D → /roles/R%26D

DoD

  • New helpers in common.rs: html_escape, encode_path_segment
  • New tests pinning both XSS regression + URL-encoding
  • All slug routes (/projects/:identifier, /roles/:name, /users/:login) now percent-encode
  • All headline_html composition escapes user-controlled strings
  • main.rs fails fast on malformed RM_BIND

All three CI gates green: fmt + clippy + cargo test --workspace (108 tests across 5 crates).

🤖 Generated with Claude Code

…ail-fast

Closes the codex items left open across my Phase 0/1 PRs:

# P1 — XSS via raw user content in handler-built HTML (#10, #12)

`wrap_in_doc(title, body)` interpolated the raw `title` straight
into the document's `<title>` element. With user-controlled inputs
(issue subjects, time-entry spent_on, project names, user display
names) reaching this fn, a payload like
`</title><script>alert(1)</script>` is a stored XSS.

The `render_detail(..., headline_html, ...)` parameter is
contractually pre-rendered HTML (kit `|safe`s it through the
template), so handlers that compose it from user data must escape
themselves. W1 (Issue), W2 (Project), W3 (TimeEntry), W4a (User),
W4b (Role) all did this with raw interpolation.

Fix:
- `common::html_escape` — minimal `& < > " '` escape, no extra dep.
- `wrap_in_doc` escapes its `title` parameter.
- Every handler that composes `headline_html` from user data now
  calls `html_escape` first.
- New regression test
  `detail_escapes_xss_in_subject_for_title_and_headline` in
  `issues.rs` exercises the full XSS surface.

# P2 — URL slug encoding (#13)

`/roles/<name>`, `/projects/<identifier>`, `/users/<login>` paths
interpolated the slug raw. A name with `#`/`?`/`/` would break the
browser's URL parse (`#` = fragment, `/` = path segment).

Fix:
- `common::encode_path_segment` — percent-encodes everything outside
  RFC-3986 unreserved + path-safe chars. No new dep (rule is small).
- Roles, projects, users all route hrefs through it.
- New regression test
  `list_percent_encodes_role_names_with_reserved_chars` in
  `roles.rs` (Q/A → Q%2FA, R&D → R%26D).

# P1 — RM_BIND fail-fast (#7)

`std::env::var("RM_BIND").ok().and_then(|s| s.parse().ok())`
collapsed unset and malformed into the same fallback. A typo
silently started the server on the default. Now we distinguish:
- Unset → default
- Malformed → return `io::Error::other(...)` immediately

# Skipped

- #9 (auth routes not reachable) — already fixed by #10's
  `build_router_with` refactor that mounts `rm_auth::router(cfg)`.
- #8 (lowercase vs PascalCase OGAR table names) — schema-
  divergence noted in W3's doc, lands in a dedicated PR; needs the
  per-port DDL emission story W4-followups will revisit.

cargo fmt --check + cargo clippy --all-targets -- -D warnings +
cargo test --workspace all green locally; 108 tests across 5 crates.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@AdaWorldAPI AdaWorldAPI merged commit a70bda9 into main Jun 21, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant