7.6 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, patterns-established, requirements-completed, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | patterns-established | requirements-completed | duration | completed | |||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-http-routing-auth-groups-and-rate-limiting | 04 | api |
|
|
|
|
|
|
|
|
|
8min | 2026-09-19 |
Phase 6 Plan 04: SSRF-guarded outbound fetch helper Summary
Dial-time SSRF-guarded fetchguard.Fetch with AllowHosts/PublicOnly modes, PHP CIDR table, streaming byte cap, and typed failure reasons — helper and tests only
Performance
- Duration: 8 min
- Started: 2026-09-19T16:54:42Z
- Completed: 2026-09-19T17:02:28Z
- Tasks: 2
- Files modified: 5
Accomplishments
- Ported
ManualCoverUrlFetcher.phpPRIVATE_V4_CIDRS/PRIVATE_V6_PREFIXESontonetip.Prefix, including CGNAT100.64.0.0/10and cloud-metadata169.254.0.0/16, with Unmap documented as the dial-hook caller's job. - Shipped
fetchguard.Fetch: https-only, host allow-list (exact + dotted-suffix soevil-discogs.comcannot matchdiscogs.com), always-on private-IP block atnet.Dialer.Control(closes PHP's admitted DNS-rebinding TOCTOU), no automatic redirects, streamingio.LimitReadercap, config defaults 10 MiB / 10s with explicit zero/negative as an error (D-14). - Zero production call sites — no
ManualCoverUrlFetcherorCoverImporterwiring (D-13).
Task Commits
Each task was committed atomically (TDD RED then GREEN):
- Task 1: Private/reserved IP classification table
f7e7ca7test(06-04): add failing test for private/reserved IP tabled3630e7feat(06-04): implement private/reserved IP classification table
- Task 2: Policy, defaults, and the dial-time-guarded Fetch entry point
bae9de8test(06-04): add failing tests for SSRF-guarded Fetch7923fe8feat(06-04): implement dial-time SSRF-guarded Fetch
Plan metadata: (this commit) docs(06-04): complete SSRF-guarded fetch helper plan
Files Created/Modified
fetchguard/ip.go—isReservedOrPrivateCIDR table (PHP constants, fail-closed on invalid addr)fetchguard/ip_test.go— table-driven cases including Unmap, CGNAT, metadata, 172.16/12 boundariesfetchguard/policy.go— Mode, Reason, Policy, Error, Defaults, DefaultsFromConfig via Lookupfetchguard/fetch.go—Fetchwith dial Control, ErrUseLastResponse, LimitReader, typed errorsfetchguard/fetch_test.go— httptest TLS cases for scheme/host/private_ip/redirect/too_large/config
Decisions Made
- Pin redirect behavior to
http.ErrUseLastResponse: a 3xx is a non-errorResultso following Location requires a newFetchthrough the same guard (T-06-17). isReservedOrPrivateclassifies an already-Unmap()'dnetip.Addr; the dial hook Unmaps, matching Pitfall 6.- Disable
HTTP_PROXYon the guarded transport so Control observes the target IP, not a proxy hop. - Unexported
Policy.tlsConfig/skipReservedChecklet tests talk to loopback httptest servers without weakening production checks.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] Unexported test-only Policy fields for httptest loopback
- Found during: Task 2 (Fetch entry point)
- Issue: httptest TLS servers bind 127.0.0.1, which the production dial-time private-IP block correctly rejects. Redirect and streaming-cap tests need a real connection to that listener; mocking
http.RoundTripperwould skip the thing under test for private_ip but would also skip LimitReader/CheckRedirect. - Fix: Unexported
Policy.tlsConfigandskipReservedCheck, set only fromfetch_test.goviawithTestLoopback. Private-IP tests leave both unset and assertReasonPrivateIPagainst the real Control hook. - Files modified:
fetchguard/policy.go,fetchguard/fetch.go,fetchguard/fetch_test.go - Verification:
go test ./fetchguard/... -race— private_ip subtests fail closed; redirect/too_large pass against real TLS listeners - Committed in:
7923fe8(Task 2 GREEN)
2. [Rule 2 - Missing Critical] Disable HTTP_PROXY on the guarded transport
- Found during: Task 2 (Fetch entry point)
- Issue: Honoring
HTTP_PROXYwould make Control see the proxy address, not the user-supplied target — an SSRF bypass. - Fix: Set
Transport.Proxyto nil (no proxy) on the per-call client. - Files modified:
fetchguard/fetch.go - Verification: private_ip tests still hit Control on 127.0.0.1;
go vet ./...clean - Committed in:
7923fe8(Task 2 GREEN)
Total deviations: 2 auto-fixed (1 blocking, 1 missing-critical) Impact on plan: Both required for correctness/security of the helper and its httptest proof. No call sites added. No scope creep.
Issues Encountered
Grok worktree clone had .git as a directory and HEAD on master. Created worktree-agent-01a0ba96 from ecab09c without rewinding master, then committed only on that branch.
User Setup Required
None - no external service configuration required.
Next Phase Readiness
fetchguard.Fetchis ready for Phase 12ManualCoverUrlFetcher(PublicOnlyMode) and Phase 14 DiscogsCoverImporter(AllowHostsMode) call sites.- Callers must pass an already-resolved
Policy(or nil cfg to use 10 MiB / 10s). Following a 3xx means a newFetchof Location. - No blockers.
TDD Gate Compliance
Per-task RED then GREEN commits are present:
f7e7ca7test(06-04) (RED Task 1)d3630e7feat(06-04) (GREEN Task 1)bae9de8test(06-04) (RED Task 2)7923fe8feat(06-04) (GREEN Task 2)
Self-Check: PASSED
- FOUND:
fetchguard/ip.go - FOUND:
fetchguard/ip_test.go - FOUND:
fetchguard/policy.go - FOUND:
fetchguard/fetch.go - FOUND:
fetchguard/fetch_test.go - FOUND:
f7e7ca7 - FOUND:
d3630e7 - FOUND:
bae9de8 - FOUND:
7923fe8 go vet ./...cleango test ./fetchguard/... -racepassgo test ./... -shortpass
Phase: 06-http-routing-auth-groups-and-rate-limiting Completed: 2026-09-19