Files
alkhttp/tasks/adapters/review-001-forward-url-safety.md
T
glm-5.3-flash 12b35e2c5f docs(tasks): decompose review 001 remediation Units 1-5 into taskgraph tasks
21 review-001 tasks across server/adapters/client/gateway/websocket/infra,
chunked from the 7-unit remediation plan in
docs/reviews/001-initial-implementation-review.md.

- Scope split by mechanism, not one-per-finding: 15 tasks in generation 1
  (parallelizable), 6 sequenced after their file-sharing precursors
- Deliberately deferred until dependent fixes land: projection/doc
  fidelity partial (Unit 6 beyond dependency hygiene), coverage backfills
  (COV-01..07 via in-task acceptance for forward.rs), and per-finding
  minors (OAI-06/07, HY-02/04/06/10/11, CON-08)
- Cross-crate WS-12 (alkcall demux 4 GiB discard alloc) noted for filing
  in alkcall, not here

taskgraph: validate clean, no cycles, 6 generations
2026-08-29 07:10:15 +00:00

64 lines
2.9 KiB
Markdown

---
id: review-001-forward-url-safety
name: Safe outbound URL construction — encoding, base-path, host validation (FWD-01, FWD-02)
status: pending
depends_on: []
scope: narrow
risk: high
impact: component
level: implementation
tags: [adapters, review-001, security]
---
## Description
Review 001 findings FWD-01 + FWD-02, both empirically verified against
`src/adapters/forward.rs` and both security-bearing **with the
operation's injected credentials attached**:
- **FWD-01**: path-parameter values are substituted raw
(`forward.rs:56-71, 133-141`). `Url::join` normalizes `..`, so
`{owner} = "../../admin"` escapes a path-scoped prefix (cross-tenant
IDOR); `?` and `#` in a value split/inject into the URL; values
containing a later placeholder get re-expanded by the iterative
substitution. Fix: percent-encode each segment
(`utf8_percent_encode` with a path-segment set), reject/encode
`?`/`#`, and render the template in a single pass (not iterative
replace). The query path is already correctly encoded via
`query_pairs_mut` — keep that shape.
- **FWD-02**: `Url::join` resolves against the base *directory*, so
`base_url = "https://api.openai.com/v1"` + `/chat/completions` silently
drops `/v1` (`forward.rs:74-78`); every test uses origin-only base
URLs so the suite can't see it. Worse: a path key that is an absolute
URL replaces scheme+host entirely (verified — reqwest only rejects
non-http(s) *schemes*), and credential injection happens after URL
construction, so a spec-controlled absolute path sends the namespace's
credentials to an arbitrary host (SSRF). Specs are
assembly-layer-supplied (trusted per ADR-066) but nothing enforces that
boundary. Fix: append to the base *path* (not origin), require the
joined URL to keep the base host (fail loudly on host change), and
require https by default (explicit opt-out for http).
## Acceptance Criteria
- [ ] Traversal test: `{owner} = "../../admin"` cannot escape the template path (segment encoded or rejected)
- [ ] `?`/`#`/later-placeholder-in-value tested (encoded or rejected, never URL-structural)
- [ ] Base URL with a path prefix keeps the prefix (`…/v1` + `/chat/completions``…/v1/chat/completions`, test)
- [ ] Absolute-URL path template is rejected loudly; joined host ≠ base host is rejected; http base refused unless opted out (tests)
- [ ] Rendering is single-pass (a rendered value is never re-substituted)
- [ ] Both `from_openapi` and `from_jsonschema` paths covered (they share `forward.rs`)
- [ ] `cargo test` and `cargo clippy --all-targets -- -D warnings` pass
## References
- docs/reviews/001-initial-implementation-review.md (Part D, FWD-01, FWD-02)
- docs/architecture/decisions/066-from-jsonschema-as-http-adapter.md
## Notes
> Agent fills during implementation. One of the two gate tasks for
> any deployment-facing milestone (with review-001-client-timeout-retry).
## Summary
> Filled on completion.