fix: trust gRPC metadata over payload session vars; mask internal gRPC errors#79
Conversation
The gRPC transport let the request payload `session_variables` override transport metadata when building the `Session`. Behind a trusted gateway that injects authenticated identity headers as gRPC metadata, a client could spoof identity by putting `x-hasura-user-id` / `x-hasura-role` in the request body (e.g. claim role `admin` or impersonate another user) — the payload silently won. This is an identity-spoofing hole. Trust model (now documented loudly on `Session`, the HTTP/gRPC entry points, and the README "Security / Trust Boundary" section): the framework does NOT authenticate. A trusted proxy must strip client-supplied `x-hasura-*` headers and inject only authenticated ones. Transport metadata/headers are trusted; the request payload is not. Changes: - gRPC `build_session`: apply payload vars first, then let trusted metadata overwrite colliding keys. Metadata now wins. Payload-only keys still pass through (preserves the Hasura-action path where verified claims arrive in the payload with no metadata injected). - gRPC errors: route the response body through a shared `HandlerError::client_facing_message()`, masking internal (5xx) detail to "Internal server error" and logging the original server-side. Previously gRPC returned raw `e.to_string()`, which could leak SQL/driver detail — HTTP already masked. The masking policy now lives in one place (error.rs) and both transports reuse it (no duplicated logic). - Docs: rustdoc trust-boundary notes on `Session`, `session_from_headers`, and `build_session`; README HTTP/gRPC transport notes plus a dedicated "Security / Trust Boundary" section. - Tests: gRPC metadata-wins-over-payload (anti-spoof) and payload-applies-when-metadata-absent; HTTP client-supplied identity header trusted verbatim (documents why the proxy is required). Implements [[tasks/grpc-session-metadata-precedence]] Also covers the gRPC error-masking item from [[tasks/transport-ingress-security-hardening]] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 56 minutes and 48 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Resolve the collision between #84's transport/ingress hardening and #79 (already on main), which independently added an internal-error masking helper and the README Security / Trust Boundary section. - src/microsvc/error.rs: drop #84's duplicate `redacted_message()` and its `is_internal()` helper; keep #79's canonical `client_facing_message()` as the single masking method (mask 5xx -> "Internal server error", keep 4xx descriptive). Behaviour is identical: `status_code() >= 500` covers exactly Repository/Other. Doc updated to note the Knative ingress also routes through it. - src/microsvc/http.rs: take #79's `client_facing_message()` call site and test names (the only difference from #84 was the method name). - src/microsvc/knative_ingress.rs: #84's masking now uses the shared `client_facing_message()`, and the 5xx log gate uses `status_code() >= 500` in place of the removed `is_internal()`. Message-name validation and the body limit are preserved. - README.md: #84 made no README edits on this branch, so the merge keeps #79's Security / Trust Boundary section verbatim; no duplication. #84's transport/ingress hardening (S3-S6) remains intact: message-name validation, HTTP/Knative body limits, inbox purge/retention, DDL allowlist, identity validation, and the payload_bitcode caveat. Verified: cargo fmt; build default + http + sqlite + postgres; test --features sqlite and --features http (knative_cloudevents + microsvc suites green with the single shared masking helper). Refines [[tasks/transport-ingress-security-hardening]]
Security summary
The gRPC transport built the request
Sessionby letting the payloadsession_variablesoverride transport metadata. Behind a trusted gateway that injects authenticated identity as gRPC metadata, a client could spoof identity via the request body.Exploit (before)
Behavior (after)
Payload vars are applied first, then trusted metadata overwrites colliding keys — metadata wins. A client can no longer override a proxy-injected
x-hasura-user-id/x-hasura-rolefrom the body. Payload-only keys (no metadata collision) still pass through, preserving the Hasura-action path where Hasura forwards verified claims in the payload and no metadata is injected.Error masking (consistency fix)
gRPC previously returned raw
e.to_string()to clients, which can leak SQL/driver detail; HTTP already masked internal errors. Both transports now route response bodies through a single sharedHandlerError::client_facing_message():"Internal server error", original logged server-side viaeprintln!(the existing sink HTTP already used).No duplicated logic: the old
http::error_message_for_responsewas removed and folded into the shared method onHandlerError.Trust boundary docs
The framework does not authenticate. A trusted proxy must strip client-supplied
x-hasura-*headers/metadata and inject only authenticated ones. Documented in:Session(framework-wide trust model + source precedence).build_sessionand HTTPsession_from_headers.Testing
cargo fmt --allcargo build --features grpc/cargo build --features http— cleancargo test --features grpc— 24 microsvc integration + 250 lib unit, all passcargo test --features http— 23 microsvc integration + 261 lib unit, all passNew tests:
grpc_payload_session_variables_do_not_override_metadata— metadatatrusted-userwins over payloadattacker.grpc_payload_session_variables_apply_when_metadata_absent— payload-only identity still flows.client_supplied_identity_header_is_trusted_verbatim(HTTP) — documents that headers are trusted as-is, which is why the proxy is required.🤖 Generated with Claude Code