Skip to content

fix: harden transport and HTTP ingress against hostile input (S3-S6 + body limits)#84

Merged
patrickleet merged 2 commits into
mainfrom
hardening/transport-ingress-hardening
Jun 11, 2026
Merged

fix: harden transport and HTTP ingress against hostile input (S3-S6 + body limits)#84
patrickleet merged 2 commits into
mainfrom
hardening/transport-ingress-hardening

Conversation

@patrickleet

Copy link
Copy Markdown
Collaborator

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-precedence PR — src/microsvc/grpc.rs is untouched here.

Per-item summary

S3 — Knative ingress error leak

knative_ingress.rs returned err.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 via HandlerError::is_internal(). Rather than copy-paste the HTTP ingress's masking logic, I promoted it into a shared pub(crate) HandlerError::redacted_message() in microsvc/error.rs; http.rs now calls it too (its private error_message_for_response is removed). Single source of truth.

Collision note: the grpc-session-precedence PR may also add a masking helper to microsvc/error.rs. If both land, expect a small merge conflict there — the intent (one shared redaction helper on HandlerError) should make resolution trivial.

S4 — Message name validation

Message.name becomes the NATS subject suffix, Kafka topic, and RabbitMQ routing/binding key, but had no validation. New validate_message_name (src/bus/message_name.rs, mirroring validate_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 on Message::name.

Enforced at the trust boundaries:

  • Inbound (hostile wire): the Knative ingress validates ce-type in both binary and structured modes before it becomes Message.name400 Bad Request.
  • Outbound (RabbitMQ): send_message, publish_message, and the per-event ensure_subscription binding now validate — a #/* in a topic binding key is a subscription wildcard.

Scope note: outbound validation is wired on the RabbitMQ paths (the worst case — wildcard-in-binding-key). The validator + Message::validate_name() are public so the NATS/Kafka publish paths can adopt the same check; extending enforcement to every adapter's publish entrypoint is a clean follow-up. Inbound (the hostile-input path) is fully covered.

S5 — Unbounded inbox growth

The consumer inbox grew forever (in-memory HashSet; SQL consumer_inbox with no TTL). Added InboxStore::purge_inbox_older_than(age):

  • Postgres/SQLite issue a single bounded DELETE evaluated against the database clock (now() - interval / datetime('now', '-N seconds')) — no client/server skew.
  • HashMapRepository keeps no timestamps, so it's a documented no-op; it gains clear_inbox() as the in-memory retention equivalent, and its inbox is now marked dev-only in rustdoc.
  • QueuedRepository delegates 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.rs spliced column.default unquoted into CREATE TABLE — the one unescaped hole in an otherwise fully-quoted generator. Now validated against an allowlist: numeric literals, NULL/TRUE/FALSE/CURRENT_TIMESTAMP keywords, and properly-escaped single-quoted string literals. Anything else fails generation loudly. distributed_cli/src/atlas.rs consumes the generated SQL unchanged, so it inherits the validation.

Body limits

Pinned axum's implicit 2 MiB default to an explicit 1 MiB DefaultBodyLimit on both the command router (http.rs) and the CloudEvents ingress (knative_ingress.rs) — both buffer the whole body into memory. Shared via one microsvc::MAX_HTTP_BODY_BYTES constant.

Doc caveat

Message::payload_bitcode now carries a one-line security caveat: bitcode is not hardened against hostile input — only decode it from trusted producers; prefer payload_json for 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 sqlite and cargo test --features http — all pass.
  • New unit tests:
    • message_name: accepts dotted names, rejects empty/over-long/control/wildcard.
    • knative ingress: wildcard ce-type (binary) and wildcard type (structured) rejected.
    • http: redacted_message masks server errors / preserves client errors.
    • table::sql: allowlist accepts safe defaults, rejects unsafe ones, and create_table_statement rejects an injected default.
    • hashmap inbox: clear_inbox drops all receipts; age-purge is a no-op.
  • DB-backed inbox purge paths are exercised by the existing env-gated Postgres/SQLite suites (no new DB-only tests added); nats/kafka/rabbit integration tests remain broker-gated and rely on CI. Kafka was not built locally (needs cmake/librdkafka) — relying on CI for that feature.

🤖 Generated with Claude Code

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>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@patrickleet, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cdfe5e5-eb7c-45a7-b51f-5c8f7dc737cd

📥 Commits

Reviewing files that changed from the base of the PR and between 6d4b697 and b725439.

📒 Files selected for processing (14)
  • src/bus/message.rs
  • src/bus/message_name.rs
  • src/bus/mod.rs
  • src/bus/rabbit_bus.rs
  • src/hashmap_repo/repository.rs
  • src/microsvc/error.rs
  • src/microsvc/http.rs
  • src/microsvc/knative_ingress.rs
  • src/microsvc/mod.rs
  • src/postgres_repo/mod.rs
  • src/queued_repo/repository.rs
  • src/repository/traits.rs
  • src/sqlite_repo/mod.rs
  • src/table/sql.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch hardening/transport-ingress-hardening

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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]]
@patrickleet patrickleet merged commit 172e53b into main Jun 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant