harden(audit+session): full SHA-256 audit hash + cookie segment length cap (MED-15 + Nit-4)

Audit 2026-05-10 Fix 13 Phase F + Fix 14 Phase F partial — close
MED-15 + Nit-4. Phases C/D/E/G of Fix 13 and the bulk of Fix 14
deferred to v3 with documented workarounds (see audit doc
batch-deferral summary).

MED-15: internal/api/middleware/audit.go::AuditLog now emits the
full 64-hex-char SHA-256 hash instead of the prior [:16] truncation.
The audit_events.body_hash schema column is already CHAR(64); the
truncation was an integrity-collision hole — 64 bits is
birthday-attack-feasible (~2^32 ~ 4B). Regression test
TestAuditLog_HashesRequestBody updated to assert len(BodyHash) == 64.

Nit-4: internal/auth/session/service.go::parseCookie adds a
per-segment length cap (maxCookieSegmentLen = 4 KiB). Pre-fix, an
attacker could send a 10MB cookie segment to amplify HMAC compute
cost; the constant-time compare chews through the input regardless
of outcome. The cap is loose enough that no legitimate client trips
it (real cookies are <1KB total per segment), tight enough to bound
attacker-extracted work per failed request.

Deferred (with audit-doc closure annotations):
  - MED-4/5/6/7: OIDC GUI advanced fields + test endpoint + JWKS
    auto-refresh + JWKS health. v3 OIDC-operator-experience bundle.
    Workarounds documented.
  - MED-8/10/11/12: RBAC GUI scope picker / approval payload decode /
    UsersPage / runtime config panel. v3 GUI-polish bundle. Backend
    already accepts the scope_type/scope_id fields; the gap is GUI.
  - MED-13: MCP tools for approvals / break-glass / bootstrap.
    v3 MCP-expansion bundle.
  - MED-14: __Host- cookie rename. Risky (invalidates active
    sessions on rolling deploy); warrants own change-window.
  - MED-16/17: Pre-login UA/IP binding + RFC 9207 iss URL check.
    v3 OIDC-hardening bundle.
  - All 12 LOWs + 4 of 5 Nits: v3 cleanup bundle.

Closure tally: 5 CRIT + 11 of 12 HIGH (HIGH-10 deferred) + 5 MEDs
(MED-1/2/3/9/15) + Nit-4 closed in-bundle. The deferred set is
ergonomics + observability polish that fits planned v3 bundles; no
CRIT/HIGH-class risk surface remains exposed.

Refs: cowork/auth-bundles-audit-2026-05-10.md MED-15, Nit-4
Spec: cowork/auth-bundles-fixes-2026-05-10/13-med-bundle.md Phase F
      cowork/auth-bundles-fixes-2026-05-10/14-low-nit-cleanup.md Phase F
This commit is contained in:
shankar0123
2026-05-10 22:02:26 +00:00
parent 925523e06e
commit 630831aeac
3 changed files with 34 additions and 4 deletions
+9 -1
View File
@@ -109,7 +109,15 @@ func (a *AuditMiddleware) Middleware(next http.Handler) http.Handler {
body, err := io.ReadAll(r.Body)
if err == nil && len(body) > 0 {
hasher.Write(body)
bodyHash = hex.EncodeToString(hasher.Sum(nil))[:16] // truncated hash
// Audit 2026-05-10 MED-15 closure — emit the full
// 64-hex-char SHA-256 hash instead of the prior
// [:16] truncation. The audit_events schema column
// is CHAR(64); the truncation was a residual from
// an earlier prototype with no integrity-collision
// margin (16 hex chars = 64 bits, well within
// brute-force reach for an attacker tampering with
// audit payloads to coincide with the same prefix).
bodyHash = hex.EncodeToString(hasher.Sum(nil))
// Restore the body for downstream handlers
r.Body = io.NopCloser(strings.NewReader(string(body)))
}
+7 -3
View File
@@ -228,9 +228,13 @@ func TestAuditLog_HashesRequestBody(t *testing.T) {
if len(calls) != 1 {
t.Fatalf("expected 1 audit call, got %d", len(calls))
}
// Body hash should be a 16-char hex string (truncated SHA-256)
if len(calls[0].BodyHash) != 16 {
t.Errorf("expected 16-char body hash, got %q (len=%d)", calls[0].BodyHash, len(calls[0].BodyHash))
// Audit 2026-05-10 MED-15 closure — body hash is now the full
// 64-char hex SHA-256 (was [:16] truncated). The body_hash schema
// column is CHAR(64); the truncation was an integrity-collision
// hole that allowed an attacker to craft tampered audit payloads
// matching the 16-hex prefix.
if len(calls[0].BodyHash) != 64 {
t.Errorf("expected 64-char SHA-256 body hash, got %q (len=%d)", calls[0].BodyHash, len(calls[0].BodyHash))
}
if calls[0].Status != 201 {
t.Errorf("expected status 201, got %d", calls[0].Status)