Skip to content

harden: request body cap, promo-preview validation, health rate-limit exempt, dev-log scrub#52

Merged
kashkoool merged 2 commits into
mainfrom
harden/endpoint-polish
Jun 4, 2026
Merged

harden: request body cap, promo-preview validation, health rate-limit exempt, dev-log scrub#52
kashkoool merged 2 commits into
mainfrom
harden/endpoint-polish

Conversation

@kashkoool

@kashkoool kashkoool commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Residual hardening from the full per-endpoint security scan. The scan confirmed the public reads and auth flows leak no PII (companies = id+name, reviews = display-name only, seat-map = seat numbers only) and no tokens are logged; these are the remaining polish items.

Changes

  • Request body cap (DoS): Kestrel MaxRequestBodySize set to 1 MB (no file-upload endpoints exist; the 30 MB default was excessive). Ignored under the test server.
  • promo-preview validation: new PreviewPromoValidator (code <= MaxCodeLength, seats 1..10) wired into GET /api/bookings/promo-preview — a malformed/over-long code is now a clean 400, not unbounded processing.
  • Health probes exempt from rate limiting: .DisableRateLimiting() on /health and /health/ready so orchestrator polls are never throttled/starved.
  • Strict dev-log scrub: the dev (no-SMTP) LoggingEmailSender no longer logs the email body / verify-reset link/token — only the subject + a masked recipient (use Mailpit to view the link locally). DevDataSeeder masks the email too. New EmailMask helper. (Both are dev-only; production is forced to real SMTP by StartupGuards.)

Explicitly left as-is (confirmed safe / by design)

  • Refresh token in the auth response body is the deliberate native-client design (also set as an HttpOnly+Secure+SameSite cookie); never logged.
  • A company seeing the email on its own bookings (desk list / booking report) is legitimate tenant data for contacting the booker — company-scoped, not a leak.
  • CORS allow-list + AllowCredentials (StartupGuards rejects *), security headers/HSTS, webhook signature verification, prod exception handler hiding internals — all verified.

Tests

1 new integration test (over-long promo code -> 400). Full suite green: 65 unit + 62 integration, build 0 warnings.

Summary by CodeRabbit

  • Bug Fixes

    • Promo preview endpoint now validates all input parameters, including trip ID, promo code length, and seat count, rejecting invalid requests.
    • Health check endpoints are exempt from rate limiting to ensure reliable health monitoring.
  • Security

    • Request body size capped at 1 MB to prevent oversized-payload attacks.
    • Email addresses masked in system logs.
  • Tests

    • Added validation test for promo code endpoint.

… exempt, dev-log PII scrub

Residual items from the full per-endpoint security scan (the scan confirmed the
public reads + auth flows leak no PII and no tokens are logged):
- Cap request bodies at 1 MB via Kestrel (no file-upload endpoints exist) to block
  oversized-payload DoS; the 30 MB default was unnecessarily large.
- Validate the promo-preview query (code <= MaxCodeLength, seats 1..10) so a
  malformed/over-long code is a clean 400, not unbounded work.
- Exempt /health + /health/ready from the rate limiter so orchestrator probes are
  never throttled or starved by other traffic.
- Strict dev-log scrub: the dev (no-SMTP) email sink no longer logs the body /
  verify-reset link/token — only the subject + a masked recipient (use Mailpit to
  view the link locally). DevDataSeeder masks the email too. New EmailMask helper.

1 new integration test (over-long promo code -> 400). Full suite green
(65 unit + 62 integration), build 0 warnings.
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

More reviews will be available in 50 minutes and 31 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 @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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2616bf0c-50c1-4483-a8cc-1bf4c996b87c

📥 Commits

Reviewing files that changed from the base of the PR and between fdd53e1 and dd0771b.

📒 Files selected for processing (2)
  • src/TransportPlatform.Api/Program.cs
  • src/TransportPlatform.Application/Promotions/PreviewPromo.cs
📝 Walkthrough

Walkthrough

This PR hardens the API server through four coordinated changes: masking email addresses in logs to prevent sensitive data leakage, validating promo code input to reject oversized payloads, enforcing a 1 MB request body size limit at the Kestrel level, and disabling rate limiting on health check endpoints to ensure orchestrator liveness probes are not throttled.

Changes

Server Hardening Audit Implementation

Layer / File(s) Summary
Email masking infrastructure and integration
src/TransportPlatform.Infrastructure/Email/EmailMask.cs, src/TransportPlatform.Infrastructure/Email/LoggingEmailSender.cs, src/TransportPlatform.Infrastructure/Persistence/DevDataSeeder.cs
EmailMask utility masks email addresses to prevent raw email logging. LoggingEmailSender uses masked recipients and omits email body from dev logs. DevDataSeeder applies masking to user-creation failure logs.
Promo code input validation and testing
src/TransportPlatform.Application/Promotions/PreviewPromo.cs, src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs, tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs
PreviewPromoValidator enforces non-empty trip ID and code, caps code length to PromoCode.MaxCodeLength, and constrains seats to 1–10. Endpoint validates queries before handler execution. Integration test verifies overlong codes are rejected with HTTP 400.
Request size limits and health check optimization
src/TransportPlatform.Api/Program.cs
Kestrel request body size capped at 1 MB to mitigate oversized-payload DoS. Health check endpoints /health and /health/ready have rate limiting disabled to prevent orchestrator probe starvation.
Audit checklist documentation
PROJECT_STATUS.md
Updates hardening scope to include work item #51, adds explicit checklist item for sensitive data protection, and increments integration test count from 61 to 62.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • kashkoool/Transport#40: Extends the promo-preview functionality with explicit input validation and rejection of oversized codes via new PreviewPromoValidator.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title comprehensively summarizes the four main hardening objectives: request body capping, promo-preview validation, health endpoint rate-limit exemption, and development logging scrubbing—all present and verifiable in the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch harden/endpoint-polish

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs (1)

77-77: ⚡ Quick win

Make the overlong input derive from the domain max-length constant.

Using a fixed 100 makes this test brittle if PromoCode.MaxCodeLength changes. Deriving from the constant keeps the test aligned with the contract it verifies.

Suggested fix
+using TransportPlatform.Domain.Promotions;
 ...
-        var longCode = new string('A', 100);
+        var longCode = new string('A', PromoCode.MaxCodeLength + 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs` at line 77,
Replace the hardcoded 100 with a value derived from the domain constant so the
test stays correct if the limit changes: construct longCode using
PromoCode.MaxCodeLength (e.g., new string('A', PromoCode.MaxCodeLength + 1))
instead of 100 in the test where longCode is defined to ensure the input is one
character longer than the allowed maximum.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/TransportPlatform.Api/Program.cs`:
- Around line 236-241: The /health/ready endpoint currently maps via
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = ...
}).DisableRateLimiting() and runs DatabaseHealthCheck (which queries the DB);
remove .DisableRateLimiting() and apply the same protections used for /metrics
(e.g., require authentication or an internal network/proxy allowlist middleware)
so only trusted callers can reach MapHealthChecks("/health/ready");
alternatively, if internal-only access is desired, wrap the MapHealthChecks call
with an authorization/allowlist policy or middleware that matches the /metrics
protection and ensure DatabaseHealthCheck remains unchanged.

In `@src/TransportPlatform.Application/Promotions/PreviewPromo.cs`:
- Line 17: The current validator RuleFor(x =>
x.Code).NotEmpty().MaximumLength(PromoCode.MaxCodeLength) checks raw length
instead of trimmed length; change the validation on RuleFor(x => x.Code) to
validate against code.Trim().Length (e.g., use a Transform or a custom Must
predicate) so the max-length check uses the trimmed value and treat emptiness on
the trimmed value as well; reference the existing RuleFor(x => x.Code) and
PromoCode.MaxCodeLength when implementing this change.

---

Nitpick comments:
In `@tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs`:
- Line 77: Replace the hardcoded 100 with a value derived from the domain
constant so the test stays correct if the limit changes: construct longCode
using PromoCode.MaxCodeLength (e.g., new string('A', PromoCode.MaxCodeLength +
1)) instead of 100 in the test where longCode is defined to ensure the input is
one character longer than the allowed maximum.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 33fcf191-96f8-44ab-b06a-5c6a4aa04ae3

📥 Commits

Reviewing files that changed from the base of the PR and between 45909ef and fdd53e1.

📒 Files selected for processing (8)
  • PROJECT_STATUS.md
  • src/TransportPlatform.Api/Endpoints/BookingEndpoints.cs
  • src/TransportPlatform.Api/Program.cs
  • src/TransportPlatform.Application/Promotions/PreviewPromo.cs
  • src/TransportPlatform.Infrastructure/Email/EmailMask.cs
  • src/TransportPlatform.Infrastructure/Email/LoggingEmailSender.cs
  • src/TransportPlatform.Infrastructure/Persistence/DevDataSeeder.cs
  • tests/TransportPlatform.IntegrationTests/PromoCodeTests.cs

Comment thread src/TransportPlatform.Api/Program.cs Outdated
Comment on lines +236 to +241
// Exempt probes from the limiter so an orchestrator's frequent liveness/readiness polls are never
// throttled (and can't be starved by other traffic sharing the IP bucket).
app.MapHealthChecks("/health", new HealthCheckOptions { Predicate = _ => false }).DisableRateLimiting();
// Readiness: runs the "ready"-tagged checks (DB connectivity). Used by orchestrator readiness
// probes / compose healthchecks to gate traffic until dependencies are actually reachable.
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") }).DisableRateLimiting();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify DatabaseHealthCheck is lightweight and doesn't create new connections on every call

# Search for the DatabaseHealthCheck class implementation
ast-grep --pattern $'class DatabaseHealthCheck {
  $$$
}'

# Also check for any IHealthCheck implementations to see the pattern
rg -n -A 10 'class.*HealthCheck.*:.*IHealthCheck'

Repository: kashkoool/Transport

Length of output: 1114


Protect /health/ready from untrusted access and reconsider disabling rate limiting
/health is effectively safe because its predicate selects no checks, but /health/ready executes DatabaseHealthCheck on every probe (await db.Companies.AnyAsync(cancellationToken)) while .DisableRateLimiting() is enabled—so if it’s reachable by non-internal clients it can directly hammer the database. Restrict /health/ready (auth and/or internal network/proxy allowlist) to the same level as /metrics rather than relying on “internal” assumptions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/TransportPlatform.Api/Program.cs` around lines 236 - 241, The
/health/ready endpoint currently maps via app.MapHealthChecks("/health/ready",
new HealthCheckOptions { Predicate = ... }).DisableRateLimiting() and runs
DatabaseHealthCheck (which queries the DB); remove .DisableRateLimiting() and
apply the same protections used for /metrics (e.g., require authentication or an
internal network/proxy allowlist middleware) so only trusted callers can reach
MapHealthChecks("/health/ready"); alternatively, if internal-only access is
desired, wrap the MapHealthChecks call with an authorization/allowlist policy or
middleware that matches the /metrics protection and ensure DatabaseHealthCheck
remains unchanged.

Comment thread src/TransportPlatform.Application/Promotions/PreviewPromo.cs Outdated
…e length

CodeRabbit follow-ups:
- /health/ready touches the DB, so it stays under the global limiter (only the
  no-I/O /health liveness probe is exempted) — prevents hammering the DB check.
- Promo-preview code validation now checks the TRIMMED length, matching the
  domain's PromoCode contract (no false 400 for surrounding whitespace).
@kashkoool kashkoool merged commit b2c88ea into main Jun 4, 2026
9 checks passed
@kashkoool kashkoool deleted the harden/endpoint-polish branch June 4, 2026 13:07
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