11 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, gap_closure, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | gap_closure | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 06-http-routing-auth-groups-and-rate-limiting | 12 | execute | 1 |
|
true | true |
|
|
Purpose: shared infrastructure must fail closed. Output: edits to surf/router.go, surf/limiter.go, bouncer/registry.go, bouncer/jwt.go. Comprehensive tests are Plan 06-14; this plan only fixes existing tests that the stricter validation legitimately breaks and may add one smoke test per fix.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@CLAUDE.md @.planning/STATE.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-VERIFICATION.md @.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-CONTEXT.md @surf/router.go @surf/bodylimit.go @surf/limiter.go @bouncer/registry.go @bouncer/jwt.go Task 1: Router ordering, factory cache, body-config validation, mux conflict error (surf/router.go) surf/router.go, surf/bodylimit.go surf/router.go (wrap lines 353-401, compile 338-351, BuildRouter 414-495), surf/bodylimit.go, compass/config.go (Lookup, Int at ~113-140), 06-VERIFICATION.md gaps 1 and warnings In Router.wrap, move the bodyLimit application: remove it from directly around the terminal handler and apply it after the named-middleware loop finishes (so it is the outermost wrapper of all named/factory middleware) but before locale(h) and the recoverJSON/recoverBare wrapping (per D-verification gap 1, so panics inside body-consuming middleware are still recovered and raw routes still get limit 0 from routeBodyLimit). Keep orgSlot and constrain innermost. Update the comment in the body.limit factory registration in BuildRouter to say the limit is applied outermost-inside-recovery in wrap().Build middleware factories once: add a per-Router cache map keyed by the full "base:param" name holding the constructed pact.Middleware (initialize in New). In wrap, look up the cache before calling factory.fn(param); store after first construction. BuildRouter's validation pass and compile's second wrap then reuse the same instances. Validation (ValidateThrottle, parseBodyLimit) still runs for every route.
Body config fail-boot: in BuildRouter when app != nil and app.Config != nil, read http.body_limits.default_bytes and http.body_limits.upload_bytes with Config.Lookup; if either is absent, not numeric, or converts to a value < 1, return an error naming the key (do not fall through to zero). Convert via a small helper that accepts int, int64, uint64 and float64 whole values. When app or app.Config is nil keep the existing zero (no config source at all) behaviour used by unit tests. If existing tests build a Config without these keys, add the two keys to those test configs rather than weakening the check.
Route conflict: in compile, register each route through a helper that defers recover() around mux.Handle and returns fmt.Errorf("surf: route conflict for %s (plugin %q): %v", ...). compile then returns that error instead of panicking.
go vet ./surf/... && go test ./surf/... -count=1 -short
<acceptance_criteria>
- grep of surf/router.go shows bodyLimit( is called after the for i := len(rt.middleware) - 1 loop and before h = locale(h)
- A quick smoke test (added to surf/bodylimit_test.go) with limit 4, named middleware doing io.ReadAll(r.Body) and a 10-byte body observes a read error (http.MaxBytesError) inside the middleware
- compile of two semantically conflicting patterns returns a non-nil error and does not panic
- BuildRouter with a Config lacking http.body_limits.default_bytes returns an error containing "default_bytes"
- go vet ./surf/... and go test ./surf/... -short exit 0
</acceptance_criteria>
Cap bounds all named middleware; factories built once; missing body config and mux conflicts are boot errors.
Middleware: remove every pass-through. When l is nil, resolve returns an error, or Key/store is nil at construction, return a middleware whose handler writes a 500 (http.StatusInternalServerError, empty body via WriteHeader only) and never calls next. This is unreachable at boot because ValidateThrottle/RegisterBucket reject the same states, but must be fail-closed if reached. Do not change the 429 wire format or headers.
Fix existing tests only where they registered now-invalid buckets or relied on pass-through, by giving them a real Key/Max/Decay and a real store (do not weaken validation). Add one smoke test asserting RegisterBucket rejects a zero-decay bucket.
go vet ./surf/... && go test ./surf/... -count=1 -race -short
<acceptance_criteria>
- RegisterBucket("p","n",Bucket{Max:1,Decay:0,Key:k}) returns non-nil error (smoke test)
- grep -n "next.ServeHTTP" surf/limiter.go shows only the post-Attempt admitted call
- Inline throttle "1,9223372036854775807" fails ValidateThrottle
- The 429 exact body/header tests already in the suite still pass
</acceptance_criteria>
No limiter state can silently disable enforcement.
jwt.go subject: for float64 reject NaN, Inf, v <= 0, v != math.Trunc(v), or v > 9007199254740992 (2^53) by returning "" ; otherwise format as int64. For json.Number reject values that do not parse with strconv.ParseInt as a positive integer (return ""); string branch unchanged. Existing legacy-equivalence tests for whole-number float subjects must keep passing.
go vet ./bouncer/... && go test ./bouncer/... -count=1 -race -short
<acceptance_criteria>
- Registering a typed-nil pointer implementing Guard returns an error (smoke test)
- A token with sub 12.5 fails authentication; sub 12 (float64) still resolves user 12
- go test ./bouncer/... -short exits 0
</acceptance_criteria>
Typed-nil guards and fractional subjects are rejected.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| client body -> named middleware | untrusted body stream read by auth/plugin middleware before any handler |
| plugin bucket definitions -> limiter | plugin-declared Max/Decay/Key define whether a security control enforces |
| signed JWT claim -> user id | numeric subject converted to a lookup key |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-06-28 | Denial of Service | surf/router.go wrap | mitigate | bodyLimit outermost of named middleware, inside recovery; regression in 06-14 |
| T-06-29 | Denial of Service / Elevation | surf/limiter.go | mitigate | RegisterBucket/ValidateThrottle reject nil store/Key, Max<1, Decay<=0, overflow; Middleware fails closed |
| T-06-32 | Denial of Service | bouncer/registry.go | mitigate | reflect-based typed-nil rejection at Register |
| T-06-33 | Spoofing | bouncer/jwt.go subject | mitigate | reject fractional/non-finite/oversized numeric sub |
| T-06-34 | Denial of Service | surf/router.go compile | mitigate | recover ServeMux conflict panic into returned error |
| T-06-35 | Denial of Service | surf/router.go BuildRouter | mitigate | missing/malformed body_limits config fails boot |
| </threat_model> |
<success_criteria> All must_haves truths hold; both repositories build and test green; commit is code only (no planning docs), no co-author tags. </success_criteria>
Create `.planning/phases/06-http-routing-auth-groups-and-rate-limiting/06-12-SUMMARY.md` when done