fix: harden transport and HTTP ingress against hostile input (S3-S6 + body limits)#84
Conversation
Security hardening batch for the Knative/HTTP ingress, bus message-name routing, consumer inbox growth, generated DDL defaults, and request body limits. Excludes the gRPC error-masking item (owned by a separate PR). S3 — Knative ingress error leak: the CloudEvents ingress returned `err.to_string()` verbatim, which can carry SQL/driver/path detail. Internal faults are now masked to "Internal server error" (and logged server-side) by reusing a shared `HandlerError::redacted_message()` helper, which the HTTP ingress now also uses instead of its private masking fn — single source of truth. S4 — Message name validation: `Message.name` flows unmodified into the NATS subject, Kafka topic, and RabbitMQ routing/binding key but had no rules. Added `validate_message_name` (mirrors `validate_stable_message_id`): rejects empty, over-long (>256B), control-character-bearing, and wildcard-bearing (`*`/`#`/`>`) names; `.` stays allowed since dotted type names are the convention. Enforced inbound on the attacker-controlled `ce-type` (binary + structured) and outbound on the RabbitMQ send/publish/bind paths (a `#`/`*` in a binding key is a subscription wildcard). `.` routing semantics documented on `Message::name`. S5 — Unbounded inbox growth: added `InboxStore::purge_inbox_older_than` (Postgres/SQLite issue a DB-clock-relative bounded DELETE; HashMap is a documented no-op). HashMapRepository gains `clear_inbox` as the in-memory equivalent and its inbox is now marked dev-only in rustdoc. Retention is documented as the operator's responsibility. S6 — Raw DEFAULT in generated DDL: the table SQL generator spliced `column.default` unquoted — the one unescaped hole in an otherwise fully quoted generator. Now validated against an allowlist (numeric/boolean/ NULL/CURRENT_TIMESTAMP keywords or a properly-escaped single-quoted literal); anything else fails generation loudly. Atlas consumes the validated output unchanged. Body limits: pinned axum's implicit 2MiB default to an explicit 1MiB `DefaultBodyLimit` on both the command router and the CloudEvents ingress (both buffer the whole body), via one shared `MAX_HTTP_BODY_BYTES`. Doc caveat: `Message::payload_bitcode` now warns bitcode is not hardened against hostile input — decode only from trusted producers. Implements [[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 7 seconds. Learn how PR review limits work. Your organization has reached its usage spending cap. Adjust your spending cap 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 (14)
✨ 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]]
Transport/ingress security hardening batch. Each item closes a place where untrusted input reaches routing, SQL, the wire, or memory. Excludes the gRPC error-masking item, owned by the separate
grpc-session-precedencePR —src/microsvc/grpc.rsis untouched here.Per-item summary
S3 — Knative ingress error leak
knative_ingress.rsreturnederr.to_string()verbatim, which can leak SQL/driver/path detail from internal faults. Now masks internal errors to"Internal server error"(and logs the real error server-side), classifying viaHandlerError::is_internal(). Rather than copy-paste the HTTP ingress's masking logic, I promoted it into a sharedpub(crate) HandlerError::redacted_message()inmicrosvc/error.rs;http.rsnow calls it too (its privateerror_message_for_responseis removed). Single source of truth.S4 — Message name validation
Message.namebecomes the NATS subject suffix, Kafka topic, and RabbitMQ routing/binding key, but had no validation. Newvalidate_message_name(src/bus/message_name.rs, mirroringvalidate_stable_message_id): rejects empty, over-long (>256 bytes), control characters, and broker wildcards (*/#/>)..is allowed — dotted type names (order.created) are the established convention across the crate — but its routing-segment semantics are now documented onMessage::name.Enforced at the trust boundaries:
ce-typein both binary and structured modes before it becomesMessage.name→400 Bad Request.send_message,publish_message, and the per-eventensure_subscriptionbinding now validate — a#/*in a topic binding key is a subscription wildcard.S5 — Unbounded inbox growth
The consumer inbox grew forever (in-memory
HashSet; SQLconsumer_inboxwith no TTL). AddedInboxStore::purge_inbox_older_than(age):DELETEevaluated against the database clock (now() - interval/datetime('now', '-N seconds')) — no client/server skew.clear_inbox()as the in-memory retention equivalent, and its inbox is now marked dev-only in rustdoc.QueuedRepositorydelegates through to its inner store.Retention is documented as the operator's responsibility (call periodically with an age larger than the broker's max redelivery window).
S6 — Raw DEFAULT in generated DDL
table/sql.rssplicedcolumn.defaultunquoted intoCREATE TABLE— the one unescaped hole in an otherwise fully-quoted generator. Now validated against an allowlist: numeric literals,NULL/TRUE/FALSE/CURRENT_TIMESTAMPkeywords, and properly-escaped single-quoted string literals. Anything else fails generation loudly.distributed_cli/src/atlas.rsconsumes the generated SQL unchanged, so it inherits the validation.Body limits
Pinned axum's implicit 2 MiB default to an explicit 1 MiB
DefaultBodyLimiton both the command router (http.rs) and the CloudEvents ingress (knative_ingress.rs) — both buffer the whole body into memory. Shared via onemicrosvc::MAX_HTTP_BODY_BYTESconstant.Doc caveat
Message::payload_bitcodenow carries a one-line security caveat: bitcode is not hardened against hostile input — only decode it from trusted producers; preferpayload_jsonfor untrusted ingress.Testing
cargo fmt --all— clean.cargo build(default),--features http,--features sqlite,--features postgres,--features rabbitmq,--features nats— all compile.cargo test --features sqliteandcargo test --features http— all pass.message_name: accepts dotted names, rejects empty/over-long/control/wildcard.ce-type(binary) and wildcardtype(structured) rejected.http:redacted_messagemasks server errors / preserves client errors.table::sql: allowlist accepts safe defaults, rejects unsafe ones, andcreate_table_statementrejects an injected default.clear_inboxdrops all receipts; age-purge is a no-op.🤖 Generated with Claude Code