Files
zeekay ecb10c5217 fix(forward+node): canonical HTTP-over-ZAP security hardening (v0.8.8)
V4 CRITICAL: forward.handle no longer panics on hostile method/path
  (validForwardLine→400 + http.NewRequestWithContext); node.safeHandle adds a
  recover() boundary across all 5 dispatch sites (TCP dispatchLoop x2,
  getOrConnect, QUIC handleCallStream + invokeHandlerOneWay) — a handler panic
  drops one conn, never crashes the node. Proven on TCP+QUIC.
V2 HIGH: forward/identity.go single-source strip set (superset of base's
  StripIdentityHeaders + Cookie drop + X-Gateway-* sweep), applied on BOTH legs
  (serve.go backend + client.go gateway); Authorization preserved.
V6: SSE responses → 501 (buffered-stream guard).
+ consolidation: zap_crosswire_test.go proves byte-identical wire vs zap-proto/go;
  MIGRATION.md roadmap.
Red-reviewed SHIP (0 crit/high/med). Zero exported-API change — source-compatible.
2026-06-18 13:39:56 -07:00

86 lines
3.3 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package forward
import (
"net/http"
"strings"
)
// identity.go is the single source of truth for which inbound headers may
// carry an authenticated principal across the HTTP-over-ZAP boundary.
//
// Only the EDGE may assert identity. The gateway validates the IAM JWT and
// promotes the result into the typed Forward identity fields (TenantID,
// UserID, IsAdmin, Permissions); the backend re-injects those as the
// canonical X-* headers AFTER stripping every client-supplied copy. A
// client that sets any identity header directly must never have it survive
// into a backend handler.
//
// This list mirrors the authoritative set in
// github.com/hanzoai/base/tools/claims.StripIdentityHeaders. The two MUST
// stay in lock-step: base strips on its own ingress as defence-in-depth,
// and this contract strips on BOTH legs (gateway-side in client.go before
// the envelope is built, backend-side in serve.go before identity is
// injected) so a forged identity header never enters the envelope and never
// reaches a handler even if the envelope is crafted directly.
var clientIdentityHeaders = []string{
// Canonical edge-injected identity (this contract's own 4).
HeaderOrgID, // X-Org-Id
HeaderUserID, // X-User-Id
HeaderUserIsAdmin, // X-User-IsAdmin
HeaderUserPerms, // X-User-Permissions
// Base's canonical roles header + gateway auxiliaries (JWT derivatives).
"X-Roles",
"X-User-Email",
"X-Phone-Number",
// Non-canonical legacy identity headers shipped across the ecosystem.
"X-User-Role", // singular
"X-User-Roles", // plural — renamed to X-Roles
"X-User-Name",
"X-Tenant-Id",
"X-Org",
"X-Is-Admin",
// Pre-validation hints from older gateway flows. The X-Gateway-* prefix
// is also caught by the prefix sweep below; listed here for parity with
// base's authoritative set.
"X-Gateway-Validated",
"X-Gateway-User-Id",
"X-Gateway-Org-Id",
"X-Gateway-User-Email",
}
// stripClientIdentity removes every client-supplied identity-bearing header
// from h. It deletes the explicit list above (case-insensitive via
// http.Header's canonicalization) and, unconditionally, every header whose
// name starts with "X-IAM-", "X-Hanzo-", or "X-Gateway-" — closing the
// "clever-new-prefix" smuggling vector. It also drops the Cookie header
// entirely: cookies can carry session/identity material the edge has
// already lifted into the typed fields, and the backend's auth path is the
// forwarded Authorization Bearer JWT, not cookies, so a forwarded Cookie is
// only a smuggling surface here.
//
// Authorization is deliberately PRESERVED: base validates the forwarded
// Bearer JWT — that is the real authentication path and must survive.
func stripClientIdentity(h http.Header) {
for _, name := range clientIdentityHeaders {
h.Del(name)
}
for key := range h {
upper := strings.ToUpper(key)
if strings.HasPrefix(upper, "X-IAM-") ||
strings.HasPrefix(upper, "X-HANZO-") ||
strings.HasPrefix(upper, "X-GATEWAY-") {
h.Del(key)
}
}
// Cookies are an identity-bearing surface the edge does not promote;
// drop them so a forwarded Cookie cannot smuggle a session. Authorization
// (the Bearer JWT base validates) is intentionally left intact.
h.Del("Cookie")
}