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
64 lines
2.9 KiB
Markdown
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. |