31 KiB
phase, slug, status, threats_total, threats_closed, threats_open, accepted_risks, asvs_level, created, verified, reopened, reverified
| phase | slug | status | threats_total | threats_closed | threats_open | accepted_risks | asvs_level | created | verified | reopened | reverified |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 06 | http-routing-auth-groups-and-rate-limiting | verified | 34 | 34 | 0 | 4 | 1 | 2026-09-19 | 2026-09-21 | 2026-09-21 | 2026-09-21 |
Phase 6 — Security Review
Guard registry, dual-group auth, atomic rate limiting, raw-group house-middleware refusal and transactional panic recovery, CORS/body-limit scoping, transition-aware SSRF protection, and exact personal-token denial serialization. Every reviewed ID from Plans 06-01 through 06-10 is mapped below to a named passing test or a restated accept rationale; Plan 06-11 refreshes the review only after both repositories pass their complete race and vet gates. Unmapped IDs are a review gap, not an accepted risk.
Date: 2026-09-21
Scope: Plans 06-01 through 06-10 (implementation, coverage, and corrective gap closure), the Plan 06-11 review refresh (superseded, see Reopened), and gap-closure Plans 06-12 through 06-14 (T-06-28 through T-06-35).
Repos grepped: summercms.go and fonoteka.go (excluding .planning/ and vendor/).
Reopened
The 2026-09-21 verdict of 26 closed / 0 open (Plan 06-11) was contradicted by phase verification: named middleware could read past the body cap, invalid limiter definitions failed open, the SSRF classifier missed IANA special-use ranges and zoned IPv6, and several warning-class defects existed. That verdict is superseded; its audit-trail row is retained below. Plans 06-12 and 06-13 fixed the code and Plan 06-14 added the regression proof, so T-06-28 through T-06-35 were added, and T-06-12 is annotated below.
Verdict Summary
The register contains 34 total threats: 34 closed, 0 open, with 4 unchanged accepted risks and no new accepted risk. This verdict follows the 2026-09-21 Plan 06-14 gate run: go vet ./... and go test ./... -count=1 -race -short passed in both summercms.go and fonoteka.go, and every test cited for T-06-28 through T-06-35 was confirmed to exist and pass.
Trust Boundaries
| Boundary | Description | Data Crossing |
|---|---|---|
| client → Authorization header | untrusted JWT or inv_ bearer parsed on every request |
raw token, token hash, users.id |
| guard registry → plugin Boot | plugin-declared guard names become live auth middleware | jwt, inv_token |
inv_token guard → golem15_fonoteka_api_tokens |
hash-indexed lookup of an untrusted bearer | token_hash, scopes, expiry, revocation |
| client → X-Forwarded-For / limiter keys | untrusted IP / token id / route param feeds the Store | RemoteAddr, XFF, tok:<id> |
| unauthenticated client → personal-token route | missing or invalid bearer traffic must consume a bounded per-IP budget before scope denial returns | bearer status, client IP, limiter counter |
| inv_token context → limiter key resolver | valid credentials must be resolved before rate limiting to retain independent token budgets | bouncer.Credential, tok:<id> |
| concurrent requests → limiter admission | threshold comparison and admitted increment must be one atomic decision | fixed-window count, maximum, retry duration |
| request metadata → anonymous inline key | caller-controlled throttle text and Host inputs must not select a fresh budget | constant inline:domainless, trusted-proxy ClientIP |
| public-share group → anonymous caller | zero-credential surface; 429 bodies must not leak internals | Retry-After, JSON error body |
| raw group → house middleware | RFC/OAuth surface must never inherit the house envelope | inv.must-change-password |
| handler/middleware → client response | status, headers, and body remain private until successful handler completion | buffered response, success commit, panic discard |
| request body → handler | unbounded POST is a resource-exhaustion vector | http.MaxBytesReader |
| caller-supplied URL → outbound fetch | user/third-party URL must never reach loopback, RFC1918, CGNAT, or metadata | dial-time IP, host allow-list |
| IPv6 transition syntax → IPv4 SSRF policy | embedded IPv4 in NAT64 and 6to4 must receive the ordinary reserved/private classification | RFC 6052 /96 and /48, RFC 3056 2002::/16 |
| request body → named middleware | a body-consuming named middleware must be bounded by the same cap as the terminal handler | http.MaxBytesReader, io.ReadAll in middleware |
| plugin bucket definition → limiter | a plugin-supplied bucket or inline throttle must not fail open at runtime | Bucket{Key, Max, Decay}, N,M param |
| non-public IP representations → dial | zoned, special-use and mapped forms must classify as their non-public form at connect time | netip.Addr incl. zone, IANA special-use prefixes |
| personal-token context → denial serializer | status and exact JSON bytes cross the public compatibility boundary together | wire.WriteJSON, raw 401/403 bytes |
Threat Register
| Threat ID | Category | Plan of origin | Disposition | Proof |
|---|---|---|---|---|
| T-06-01 | Spoofing | 06-01 | mitigate | bouncer/registry_test.go:TestDuplicateGuardNameFailsWithPluginAndName; bouncer/registry_test.go:TestUnknownGuardNameFails; bouncer/registry_test.go:TestRegisterNeitherInterfaceNamesPluginAndName |
| T-06-02 | Elevation of Privilege | 06-01 | mitigate | plugins/golem15/fonoteka/routes_isolation_test.go:TestFullRouteTableAuthGroupMutualExclusivity; plugins/golem15/fonoteka/routes_group_test.go:TestGenresSharedHandler |
| T-06-03 | Information Disclosure | 06-01 | accept | Already hidden via json:"-" and Hidden() (Phase 5, verified by 05-06's hidden-marshal test); this plan adds no new serialization path for the hash |
| T-06-04 | Repudiation | 06-01 | mitigate | plugins/golem15/fonoteka/classes/auth/token_guard_test.go:TestTokenGuard (valid-stamps-once); grep of the auth package finds no fmt/log of the raw bearer |
| T-06-05 | Tampering | 06-01 | accept | Indexed equality lookup (not a byte-for-byte secret compare) is not a timing side-channel per RESEARCH.md's V6 Cryptography note; crypto/subtle is reserved for a future raw-compare path (e.g. OAuth client secrets, Phase 8), not needed here |
| T-06-06 | Denial of Service | 06-02 | mitigate | surf/clientip_test.go:TestClientIPRejectsSpoofedXFF |
| T-06-07 | Information Disclosure | 06-02 | mitigate | plugins/golem15/fonoteka/middleware/public_share_headers_test.go:TestPublicShareHeadersRewrites429 |
| T-06-08 | Denial of Service | 06-02 | accept | v1 ships an unbounded-until-swept map per CONTEXT D-03's explicit "no otter/cooler this phase" decision; the sweep goroutine bounds long-term growth to roughly one decay window's worth of distinct keys, acceptable for a single-instance v1 deployment |
| T-06-09 | Repudiation | 06-02 | mitigate | parity/php_debug_test.go:TestPHPParityPinsAppDebugFalse |
| T-06-10 | Elevation of Privilege | 06-03 | mitigate | plugins/golem15/fonoteka/routes_isolation_test.go:TestFullRouteTableAuthGroupMutualExclusivity (full assembled Router.Routes(), not a hand-built fixture) |
| T-06-11 | Tampering | 06-03 | mitigate | surf/routetable_test.go:TestRawGroupHouseMiddlewareRefusedAtBuild; plugins/golem15/fonoteka/routes_cors_test.go:TestRawGroupRefusesHouseMiddlewareOnRealPlugins; plugins/golem15/fonoteka/routes_cors_test.go:TestHouseMiddlewareCapabilityOnRealPlugins |
| T-06-12 | Denial of Service | 06-03 | mitigate | surf/bodylimit_test.go:TestBodyLimitDefaultRejectsOversizedBody; surf/bodylimit_test.go:TestBodyLimitRawExempt |
| T-06-13 | Information Disclosure | 06-03 | mitigate | http_config_test.go:TestProductionBodyLimitsOperatorConfirmed (134217728 / 134217728; no INTERIM) |
| T-06-14 | Elevation of Privilege | 06-04 | mitigate | fetchguard/fetch_test.go:TestFetchPrivateIPBlockedInBothModes; fetchguard/ip_test.go:TestIsReservedOrPrivate |
| T-06-15 | Tampering | 06-04 | mitigate | fetchguard/fetch_test.go:TestFetchPrivateIPBlockedInBothModes (dial-time net.Dialer.Control on the address being connected, not a pre-resolved hostname) |
| T-06-16 | Denial of Service | 06-04 | mitigate | fetchguard/fetch_test.go:TestFetchTooLargeIsStreaming |
| T-06-17 | Elevation of Privilege | 06-04 | mitigate | fetchguard/fetch_test.go:TestFetchDoesNotFollowRedirect |
| T-06-18 | Spoofing | 06-04 | mitigate | fetchguard/fetch_test.go:TestFetchAllowHostsRejectsDottedSuffixBypass; fetchguard/fetch_coverage_test.go:TestHostAllowedExactAndDottedSuffix |
| T-06-21 | Denial of Service | 06-06 | mitigate | plugins/golem15/fonoteka/routes_isolation_test.go:TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited drives 61 same-IP requests through the real handler returned by surf.Assemble: requests 1-60 retain 401 Invalid token, while request 61 receives the exact 429 response. The source declaration and runtime-order invariant is inv_token -> throttle:fonoteka-api-token -> inv.scope:read. |
| T-06-22 | Denial of Service | 06-06 | mitigate | The live route keeps inv_token before throttle:fonoteka-api-token, so bouncer.Credential is populated before the bucket key closure and valid credentials retain tok:<id> keying instead of collapsing onto the IP fallback. The exact ordering is covered by TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimited plus the route-source invariant. |
| T-06-23 | Denial of Service | 06-07 | mitigate | surf/limiter_test.go:TestMemoryStoreConcurrentAttempt; surf/limiter_test.go:TestFixedWindowLimiterConcurrentMaxOne; surf.MemoryStore.Attempt owns expiry, threshold comparison, admitted increment, and retry duration under one mutex. |
| T-06-24 | Denial of Service | 06-07 | mitigate | surf/limiter_test.go:TestFixedWindowLimiterInlineThrottleKeys/anonymous_same_IP_different_Host; TestFixedWindowLimiterInlineThrottleKeys/anonymous_inline_policies_share_a_domainless_key; TestFixedWindowLimiterInlineThrottleKeys/principals_differ; production anonymous key is exactly `inline:domainless |
| T-06-25 | Elevation of Privilege / Information Disclosure | 06-08 | mitigate | fetchguard/ip_test.go:TestIsReservedOrPrivateIPv6Transitions; fetchguard/fetch_test.go:TestDialControlRejectsUnsafeIPv6Transitions; fetchguard.embeddedTransitionIPv4 decodes both NAT64 forms and 6to4 before the dial decision. |
| T-06-26 | Information Disclosure | 06-09 | mitigate | surf/router_test.go:TestRecoverDiscardsPartialResponse/house; TestRecoverDiscardsPartialResponse/raw; TestBufferedResponseCommitsSuccessfulOutput; shared bufferedResponse commits only after a normal return. |
| T-06-27 | Tampering | 06-10 | mitigate | plugins/golem15/fonoteka/middleware/token_scope_test.go:TestInvScope/no-user-401; TestInvScope/missing-scope-403; both denial branches call wire.WriteJSON and compare untrimmed bytes. |
| T-06-28 | Denial of Service | 06-12 | mitigate | surf/bodylimit_test.go:TestBodyLimitBoundsBodyConsumingMiddleware; surf/bodylimit_test.go:TestBodyLimitBoundsNamedMiddleware; surf/bodylimit_test.go:TestBodyLimitInvalidParamFailsBoot; source surf/router.go wrap |
| T-06-29 | Denial of Service | 06-12 | mitigate | surf/limiter_test.go:TestRegisterBucketRejectsInvalid; surf/limiter_test.go:TestValidateThrottleRejectsOverflowAndNilStore; surf/limiter_test.go:TestMiddlewareFailsClosed; source surf/limiter.go RegisterBucket/Middleware/ValidateThrottle/resolve |
| T-06-30 | Elevation of Privilege | 06-13 | mitigate | fetchguard/ip_test.go:TestIsReservedOrPrivateIANABoundaries; fetchguard/ip_test.go:TestIsReservedOrPrivateSpecialUseSmoke; source fetchguard/ip.go privateV4/privateV6 |
| T-06-31 | Elevation of Privilege | 06-13 | mitigate | fetchguard/ip_test.go:TestIsReservedOrPrivateIgnoresZone; fetchguard/fetch_test.go:TestDialControlRejectsZonedAndSpecialUse; fetchguard/fetch_test.go:TestFetchPublicOnlyMapsSpecialUseToPrivateIP; source fetchguard/fetch.go dialControl |
| T-06-32 | Spoofing | 06-12 | mitigate | bouncer/registry_test.go:TestRegisterRejectsTypedNilGuard; bouncer/registry_test.go:TestRegisterRejectsTypedNilPointerFuncMapGuards; bouncer/registry_test.go:TestRegisterAcceptsValidGuards; source bouncer/registry.go Register |
| T-06-33 | Spoofing | 06-12 | mitigate | bouncer/jwt_test.go:TestVerifyRejectsFractionalSubject; bouncer/jwt_test.go:TestVerifySubjectMatrix; bouncer/jwt_test.go:TestSubjectJSONNumber; source bouncer/jwt.go subject |
| T-06-34 | Denial of Service | 06-12 | mitigate | surf/bodylimit_test.go:TestCompileRouteConflictReturnsError; surf/router_test.go:TestFactoriesBuiltOncePerName; source surf/router.go handleRoute |
| T-06-35 | Denial of Service | 06-12 | mitigate | surf/router_test.go:TestBuildRouterFailsOnMissingBodyConfig; surf/bodylimit_test.go:TestBodyLimitMissingConfigFailsBoot; source surf/router.go requiredBytes |
| T-06-SC | Tampering | 06-03 | accept | Both packages are STACK.md-named and pass 06-RESEARCH.md's Package Legitimacy Audit (Approved disposition, no [ASSUMED]/[SUS] verdicts) -- no additional human-verify checkpoint required beyond that prior audit |
Status: 34 closed / 0 open. Dispositions are copied from the originating plans; all accept rationales remain verbatim.
Findings by Threat
T-06-01 — duplicate or unknown guard names fail boot
- Source:
bouncer/registry.go(Register,Middleware). - Test evidence:
TestDuplicateGuardNameFailsWithPluginAndName,TestUnknownGuardNameFails,TestRegisterNeitherInterfaceNamesPluginAndName. - Finding: Empty name, nil guard, a type implementing neither
GuardnorCredentialGuard, a duplicate name, and an unknownMiddlewarelookup all return abouncer: ...error naming plugin and guard. No silent no-op auth. - Disposition: closed / mitigate.
T-06-02 / T-06-10 — jwt and inv_token groups are mutually exclusive
- Source:
plugins/golem15/fonoteka/routes.go;surf/routetable.goRoutes(). - Test evidence:
TestFullRouteTableAuthGroupMutualExclusivitywalks the realBuildRoutertable forgolem15.user+golem15.fonoteka. Zero/api/v1/fonoteka*entries carryjwt.auth; zero/_fonoteka/api/v1*entries carryinv_tokenorinv.scope:*.TestGenresSharedHandlerproves both groups reach the same handler through different guards. - Finding: Plan 06-01's partial coverage (two groups never sharing a middleware list literal) is completed over the whole assembled table, not the genres pair alone.
- Disposition: closed / mitigate.
T-06-03 — ApiToken.TokenHash serialization (accept)
- Rationale (verbatim from 06-01): Already hidden via json:"-" and Hidden() (Phase 5, verified by 05-06's hidden-marshal test); this plan adds no new serialization path for the hash.
- Supporting evidence:
classes/hidden_marshal_test.go:TestHiddenNeverMarshals/TestSecretColumnNames(token_hashis a secret column). Phase 6 added no marshal path. - Disposition: closed / accept.
T-06-04 — last_used stamp without logging the bearer
- Source:
plugins/golem15/fonoteka/classes/auth/token_guard.go(UpdateColumnsoflast_used_at/last_used_iponly). - Test evidence:
TestTokenGuard/valid-stamps-onceasserts one stamp perAuthenticateCredentialcall. - Grep:
rg -n 'fmt\.(Print\|Printf\|Println)\|log\.(Print\|Printf\|Println\|Fatal)\|slog\.'overfonoteka.go/plugins/golem15/fonoteka/classes/authandsummercms.go/bouncerreturns no matches.LastUsedIPappears only as the DB column write and test assertions.bearerTokenis local; the raw bearer is hashed then discarded.bouncer.Credentialcall sites are InvScope (type-assert + HasScope) and thefonoteka-api-tokenbucket key (tok:<id>), never a log line. - Disposition: closed / mitigate.
T-06-05 — SHA-256 hash lookup timing (accept)
- Rationale (verbatim from 06-01): Indexed equality lookup (not a byte-for-byte secret compare) is not a timing side-channel per RESEARCH.md's V6 Cryptography note; crypto/subtle is reserved for a future raw-compare path (e.g. OAuth client secrets, Phase 8), not needed here.
- Disposition: closed / accept.
T-06-06 — X-Forwarded-For spoofing
- Source:
surf/clientip.go. - Test evidence:
TestClientIPRejectsSpoofedXFF— untrustedRemoteAddrignores XFF;TestClientIPRightmostUntrustedHophonors XFF only when RemoteAddr is insidehttp.trusted_proxies. - Disposition: closed / mitigate.
T-06-07 — public-share 429 body
- Source:
plugins/golem15/fonoteka/middleware/public_share_headers.go. - Test evidence:
TestPublicShareHeadersRewrites429rewrites{"message":"Too Many Attempts."}to{"error":"Too many requests"}while preserving limiter headers and settingX-Robots-Tag/Cache-Control. - Disposition: closed / mitigate.
T-06-08 — MemoryStore cardinality (accept)
- Rationale (verbatim from 06-02): v1 ships an unbounded-until-swept map per CONTEXT D-03's explicit "no otter/cooler this phase" decision; the sweep goroutine bounds long-term growth to roughly one decay window's worth of distinct keys, acceptable for a single-instance v1 deployment.
- Supporting evidence:
surf/limiter_coverage_test.go:TestMemoryStoreSweepRemovesExpiredEntryproves the sweep actually deletes expired entries (not only the lazyTooManyAttemptspath). - Disposition: closed / accept.
T-06-09 — APP_DEBUG on recorded fixtures
- Source:
parity/php_parity.shexport APP_DEBUG=false. - Test evidence:
TestPHPParityPinsAppDebugFalse. - Finding: 06-02 audited three existing HTML-exception fixtures recorded under debug; they remain flagged for re-record and are not 429s. Future recordings are production-shaped.
- Disposition: closed / mitigate.
T-06-11 — raw group cannot take house-envelope middleware
- Source:
surf/router.gowrap();pact.HasHouseMiddleware;Plugin.HouseMiddlewares(). - Test evidence:
TestRawGroupHouseMiddlewareRefusedAtBuild,TestRawGroupRefusesHouseMiddlewareOnRealPlugins,TestHouseMiddlewareCapabilityOnRealPlugins. - Grep:
inv.must-change-passwordappears inplugin.goonly insideHouseMiddlewares()(line 75), never insideMiddlewares(). Plugins do not callRegisterHouseMiddleware/RegisterMiddleware. - Disposition: closed / mitigate.
T-06-12 / T-06-13 — body limits
-
Correction (06-14): the original proof only covered the terminal handler, so a named middleware running before the handler could read an unbounded body. See T-06-28 for the corrected proof.
-
Source:
surf/bodylimit.go;fonoteka.go/config/http.yaml. -
Test evidence:
TestBodyLimitDefaultRejectsOversizedBody(MaxBytesReader 413 on non-raw);TestBodyLimitRawExempt;TestProductionBodyLimitsOperatorConfirmed(both keys 134217728, no INTERIM). Operator-confirmed 2026-09-19 from nginxclient_max_body_size=128Mand php.inipost_max_size=128M/upload_max_filesize=128M. -
Disposition: closed / mitigate.
T-06-14 through T-06-18 — SSRF fetch helper
- Source:
fetchguard/ip.go,fetchguard/fetch.go. - Test evidence: private/reserved/CGNAT/metadata table (
TestIsReservedOrPrivate); always-on dial-time block in both modes (TestFetchPrivateIPBlockedInBothModes); streaming cap (TestFetchTooLargeIsStreaming); no automatic redirects (TestFetchDoesNotFollowRedirect); dotted-suffix allow-list (TestFetchAllowHostsRejectsDottedSuffixBypass,TestHostAllowedExactAndDottedSuffix). - Disposition: closed / mitigate.
T-06-21 — unauthenticated personal-token traffic cannot bypass the limiter
- Source:
plugins/golem15/fonoteka/routes.go, whose exact declaration isinv_token->throttle:fonoteka-api-token->inv.scope:read;surf/router.goapplies that declaration last-to-first so the same sequence is the runtime onion. - Test evidence:
TestPersonalTokenGenresUnauthenticatedRequestsAreRateLimitedcreates one fresh handler throughsurf.Assemblefor the realgolem15.user+golem15.fonotekaplugin set and drives 61 same-IP requests throughGET /api/v1/fonoteka/genres. - Finding: Requests 1-60 retain the PHP-compatible 401
{"error":"Invalid token"}response, provingInvScopestill owns denial before exhaustion. Request 61 receives exactly{"message":"Too Many Attempts."}with exhaustedX-RateLimit-*headers, proving missing credentials consume the 60/minute IP-fallback budget. - Disposition: closed / mitigate.
T-06-22 — valid credentials retain isolated token buckets
- Source:
plugins/golem15/fonoteka/routes.go;plugins/golem15/fonoteka/plugin.gofonoteka-api-tokenkey closure. - Test and invariant evidence: The executed route keeps
inv_tokenbeforethrottle:fonoteka-api-token, whileTestPersonalTokenGenresUnauthenticatedRequestsAreRateLimitedexercises the same assembled production middleware chain. The exact invariant isinv_token->throttle:fonoteka-api-token->inv.scope:read. - Finding: A valid personal token populates
bouncer.Credentialbefore the limiter resolves its key, preservingtok:<id>keying. Only missing or invalid credentials fall back tosurf.ClientIP; valid credentials do not collapse onto a shared IP budget. - Disposition: closed / mitigate.
T-06-23 — fixed-window admission is atomic under contention
- Source:
surf/limiter_store.go(Store.Attempt,MemoryStore.Attempt);surf/limiter.go(FixedWindowLimiter.Middleware). - Test evidence:
TestMemoryStoreConcurrentAttemptandTestFixedWindowLimiterConcurrentMaxOnecoordinate 32 workers behind ready/start barriers withMax=1. - Finding:
Attemptreads time once and performs lazy expiry, threshold comparison, the admitted increment, and retry-duration calculation while holding one mutex. Exactly one contender is admitted, the protected handler runs once, and the other 31 requests receive the exact 429 body without incrementing the exhausted counter. - Disposition: closed / mitigate.
T-06-24 — anonymous inline keys are server-controlled and domainless
- Source:
surf/limiter.go(FixedWindowLimiter.resolve), whose anonymous signature is exactlyinline:domainless|<ClientIP>and whose authenticated signature remainsu:<id>. - Test evidence:
TestFixedWindowLimiterInlineThrottleKeys/anonymous_same_IP_different_Hostproves Host rotation shares the exhausted bucket;TestFixedWindowLimiterInlineThrottleKeys/anonymous_inline_policies_share_a_domainless_keyproves different inline throttle parameters from the same IP share one budget;TestFixedWindowLimiterInlineThrottleKeys/principals_differproves authenticated principals retain independentu:<id>buckets. - Finding: The anonymous key explicitly excludes the throttle
param,r.Host, theForwardedhost parameter, andX-Forwarded-Host. Only the constant router-owned namespace and trusted-proxy-awareClientIPparticipate, so neither policy text nor any request/forwarded Host input can rotate anonymous buckets. - Disposition: closed / mitigate.
T-06-25 — transition-address SSRF representations receive the IPv4 policy
- Source:
fetchguard/ip.go(isReservedOrPrivate,embeddedTransitionIPv4);fetchguard/fetch.go(dialControl). - Test evidence:
TestIsReservedOrPrivateIPv6Transitionscovers loopback, RFC1918, metadata, and public controls for RFC 605264:ff9b::/96, RFC 6052 local-use64:ff9b:1::/48, and RFC 3056 6to42002::/16, including fail-closed non-zero/48uoctet handling.TestDialControlRejectsUnsafeIPv6Transitionsproves all unsafe forms returnerrPrivateIP/ReasonPrivateIPat the production connection boundary before the raw connection is used. - Finding: Supported transition formats extract an IPv4 value and recursively apply the ordinary IPv4 reserved/private table; public
8.8.8.8controls remain allowed rather than blanket-blocking the prefixes. - Disposition: closed / mitigate.
T-06-26 — partial route output is discarded on panic
- Source:
surf/router.go(bufferedResponse,recoverJSON,recoverBare). - Test evidence:
TestRecoverDiscardsPartialResponse/houseandTestRecoverDiscardsPartialResponse/raweach write status 202,X-Partial: secret, andsecret-partialbefore panicking.TestBufferedResponseCommitsSuccessfulOutputcovers explicit status, repeatedWriteHeader, implicit 200, headers, and body on success. - Finding: Both recovery wrappers pass only the private buffer to the route. A panic discards buffered status, headers, and body: house returns exact
{"error":true,"message":"Internal server error"}with status 500, while raw returns a header-clean, bodyless 500. A normal return commits once and replaces only route-owned header keys, preserving unrelated outer-wrapper headers. - Disposition: closed / mitigate.
T-06-27 — InvScope denial bytes match the PHP contract
- Source:
plugins/golem15/fonoteka/middleware/token_scope.go, where both denial branches callwire.WriteJSON. - Test evidence:
TestInvScope/no-user-401compares raw bytes exactly to{"error":"Invalid token"};TestInvScope/missing-scope-403compares raw bytes exactly to{"error":"Missing required scope: write"}. Both assert final}and reject every CR/LF byte before the secondary JSON-shape check. - Finding: The prior
json.Encoder.Encodenewline is gone; status, Content-Type, and untrimmed body bytes are one locked response contract. The valid-scope path still reaches the handler, and the wrong-credential path remains fail-closed. - Disposition: closed / mitigate.
T-06-SC — OpenAPI toolchain packages (accept)
- Rationale (verbatim from 06-03): Both packages are STACK.md-named and pass 06-RESEARCH.md's Package Legitimacy Audit (Approved disposition, no [ASSUMED]/[SUS] verdicts) -- no additional human-verify checkpoint required beyond that prior audit.
- Disposition: closed / accept.
T-06-28 — body cap bounds body-consuming named middleware
- Source:
surf/router.gowrap: thebodyLimitwrapper is applied after all named/factory middleware, so it is outermost inside recovery. - Test evidence:
TestBodyLimitBoundsBodyConsumingMiddleware(default limit,body.limit:Noverride both raising and bounding, raw route unaffected, panic after read yields clean 500 without leaking the panic text);TestBodyLimitBoundsNamedMiddleware;TestBodyLimitInvalidParamFailsBoot. - Disposition: closed / mitigate.
T-06-29 — invalid limiter definitions fail closed
- Source:
surf/limiter.go. - Test evidence:
TestRegisterBucketRejectsInvalid(nil Key, Max 0/-1, Decay 0/negative, nil store; errors name plugin and bucket);TestValidateThrottleRejectsOverflowAndNilStore;TestMiddlewareFailsClosed(misconfigured limiter answers 500 and never calls next). - Disposition: closed / mitigate.
T-06-30 — IANA special-use ranges classified non-public
- Source:
fetchguard/ip.go. - Test evidence:
TestIsReservedOrPrivateIANABoundariesasserts first, last and interior address of every listed prefix are non-public, neighbours outside all ranges are public, and IPv4-mapped forms follow the IPv4 table.isReservedOrPrivateis at 100% statement coverage. - Disposition: closed / mitigate.
T-06-31 — zoned IPv6 cannot evade prefix checks
- Source:
fetchguard/ip.go(zone stripped),fetchguard/fetch.godialControl(zoned targets rejected). - Test evidence:
TestIsReservedOrPrivateIgnoresZone,TestDialControlRejectsZonedAndSpecialUse([fe80::1%eth0], 198.18.0.1, 192.0.0.1, 240.0.0.1 allerrPrivateIP; public passes),TestFetchPublicOnlyMapsSpecialUseToPrivateIP(Fetch reasonprivate_ip, no network I/O).dialControlis at 100% statement coverage. - Disposition: closed / mitigate.
T-06-32 — typed-nil guards rejected at registration
- Test evidence:
TestRegisterRejectsTypedNilGuard,TestRegisterRejectsTypedNilPointerFuncMapGuards,TestRegisterAcceptsValidGuards. - Disposition: closed / mitigate.
T-06-33 — fractional or out-of-range JWT subject rejected
- Test evidence:
TestVerifyRejectsFractionalSubject,TestVerifySubjectMatrix(12.5, 1e300, 2^60 float, -1, 0 rejected; 12 and "12" accepted),TestSubjectJSONNumber. - Disposition: closed / mitigate.
T-06-34 — route conflicts return errors, factories built once
- Test evidence:
TestCompileRouteConflictReturnsError(no panic);TestFactoriesBuiltOncePerName. The latter initially failed: the 06-12builtcache was declared but never consulted, so factories ran once per route per pass. Fixed in Plan 06-14 commit1d2e00c(cache keyed byname:param). - Disposition: closed / mitigate.
T-06-35 — missing body config no longer becomes zero
- Test evidence:
TestBuildRouterFailsOnMissingBodyConfig(missing, zero, negative, non-numeric error; valid passes),TestBodyLimitMissingConfigFailsBoot. - Disposition: closed / mitigate.
Credential / bearer logging grep
rg -n 'raw|bearer|LastUsedIP' fonoteka.go/plugins/golem15/fonoteka/classes/auth (excluding tests): bearerToken helper, LastUsedIP column write in UpdateColumns, no adjacent fmt.Print* / log.* / slog. summercms.go/bouncer has no Print/log of the token. bouncer.Credential is read by InvScope and the named bucket key only.
House-middleware registration grep
grep -n "inv.must-change-password" fonoteka.go/plugins/golem15/fonoteka/plugin.go
75: "inv.must-change-password": middleware.MustChangePassword,
That line is inside HouseMiddlewares(). Middlewares() registers public.share-headers and inv_token only. Plan 06-03's move onto pact.HasHouseMiddleware is the only registration path.
Accepted Risks Log
Four accepts (06-05's "three" list omitted T-06-05, which 06-01 already accepted). Plans 06-06 through 06-14 add mitigated threats only and no new accepts. Rationales are copied verbatim in the Threat Register Proof column for each accept row.
Post-Gap Verification Gates
Final gates (Plan 06-14, 2026-09-21): both go vet ./... and go test ./... -count=1 -race -short passed in summercms.go and fonoteka.go.
summercms.go:go test ./... -count=1 -race -short— pass;go vet ./...— pass.fonoteka.go:go test ./... -count=1 -race -short— pass;go vet ./...— pass.- Source assertion: anonymous inline keys contain
inline:domainless|<ClientIP>and no throttle-parameter or request/forwarded-Host contribution — pass. - Evidence assertion: exactly one Threat Register row and one substantive finding exist for each of T-06-23 through T-06-27 — pass.
Security Audit Trail
| Audit Date | Threats Total | Closed | Open | Run By |
|---|---|---|---|---|
| 2026-09-19 | 19 | 19 | 0 | gsd-executor (06-05) |
| 2026-09-20 | 21 | 21 | 0 | gsd-executor (06-06) |
| 2026-09-21 | 26 | 26 | 0 | gsd-executor (06-11 post-gap refresh) -- SUPERSEDED, contradicted by verification |
| 2026-09-21 | 34 | 34 | 0 | gsd-executor (06-14 reopen and re-close; vet + race tests green in both repos) |