SEC-FIX-2 — All 8 findings resolved (commit 7d59f13)
SEC-01 DB error sanitization — Conflict & Internal responses now generic (A03)
SEC-02 RLS INSERT policies enforce tenant_id at DB layer via migration 006 (A05)
SEC-03 Auth failures logged at WARN with peer, path, reason (A10)
SEC-04 RBAC deferral documented in ADR-001 (A05)
SEC-05 Dead dependencies removed: reqwest, validator
SEC-06 .unwrap() replaced with .expect() in InMemoryProducer
SEC-07 X-Correlation-Id propagated from requests to CloudEvents (A10)
SEC-08 64KB limit added to spec multipart field (A06)
6
PASS
3
WARN
2
N / A
5
Low Findings
0
Unsafe Blocks
0
Secrets Found

OWASP Top 10 — Category Review

A01 Injection PASS
  • All SQLx queries use parameterized bindings ($1, $2) — no raw SQL string interpolation
  • RLS tenant context set via parameterized set_config('app.tenant_id', $1, true)
  • No Command::new() calls with user-controlled input found in codebase
A02 Broken Authentication PASS
  • RS256 enforced in production; HS256 requires explicit JWT_ALLOW_HS256=true opt-in
  • exp, iss, aud claims validated; HS256 secret minimum 32 chars enforced at startup
  • Algorithm downgrade prevented — RS256 config rejects HS256 tokens outright
  • LOW extractors.rs:221format!("Invalid token: {e}") passes jsonwebtoken error text verbatim into 401 response. Leaks rejection reason (e.g., "InvalidSignature", "InvalidIssuer") to callers.
A03 Sensitive Data Exposure WARN
  • FIXED Conflict and Internal errors return generic messages; details logged server-side (SEC-01)
  • LOW error.rs:34 — FK violations create PdfError::Validation(format!("Foreign key violation: {db_err}")). The Validation variant is returned verbatim to clients, exposing internal table/column relationship names. Missed by SEC-01 sanitization.
  • No hardcoded secrets in source; .gitignore covers .env, *.pem, *.key
A04 XML External Entities N/A
  • JSON-only API — no XML parsing in the codebase. PDF binary processing via lopdf does not involve XML entity parsing.
A05 Broken Access Control WARN
  • FIXED RLS INSERT policies now enforce tenant_id = current_setting('app.tenant_id')::UUID at DB layer — migration 006 (SEC-02)
  • FIXED Cross-tenant template references prevented in job creation via tenant-scoped find_by_id
  • LOW · ACCEPTED No RBAC enforcement — any authenticated tenant user has full read/write access regardless of role. Risk accepted, documented in docs/adr/001-defer-rbac-enforcement.md, deferred post-P1 pending OID role definitions.
A06 Security Misconfiguration PASS
  • FIXED spec multipart field now has 64KB limit across split, merge, and rotate handlers (SEC-08)
  • CORS default-deny when CORS_ALLOWED_ORIGINS unset; Docker runs as non-root app:app; no debug mode in production code paths
  • LOW producer.rs — RedpandaProducer has no TLS/SASL; Kafka traffic unencrypted and unauthenticated. Required before production deployment.
  • LOW main.rs — Custom migration runner silently ignores SQL errors on startup; schema inconsistency risk on failed migration mid-run.
A07 Cross-Site Scripting N/A
  • API-only service — no HTML output rendered from user-controlled data. XSS is not applicable.
A08 Insecure Deserialization PASS
  • serde/serde_json used safely throughout; all production-path .unwrap() on serde ops are inside #[cfg(test)] only
  • Size limits enforced via streaming: 1MB JSON body, 50MB PDF, max 20 files / 200MB total for merge
  • LOW templates.rsserde_json::Value for schema and input_data fields has no recursion depth limit; deeply nested JSON could exhaust stack before size checks fire.
A09 Known Vulnerabilities WARN
  • WARN cargo-audit not installed — automated CVE scan not performed. Run: cargo install cargo-audit && cargo audit
  • Manual review: rdkafka 0.36 wraps librdkafka C library (native code surface); lopdf 0.34 parses untrusted PDF bytes with reduced maintenance activity. No known CVEs at review date.
  • FIXED Dead deps reqwest and validator removed (SEC-05)
A10 Insufficient Logging PASS
  • FIXED Auth failures logged at WARN with peer address, request path, and failure reason enum (SEC-03)
  • FIXED X-Correlation-Id header propagated from incoming requests to CloudEvents correlationid field (SEC-07)
  • Structured JSON logging via tracing subscriber; no stack traces leaked to HTTP responses
  • Operation completion events log tenant_id, user_id, processing metrics for audit trail

Remaining Findings — All Low Severity

ID Category Severity Location Description
R-01 A02 LOW extractors.rs:221 JWT error text from jsonwebtoken crate passed verbatim in Unauthorized response. Reveals token rejection reason to callers (e.g., "InvalidSignature", "ExpiredSignature"). Fix: return generic "Token validation failed"; log detail at WARN server-side.
R-02 A03 LOW error.rs:34 FK violations create PdfError::Validation(format!("Foreign key violation: {db_err}")). Validation variant returned verbatim — exposes constraint names and table relationships. Fix: sanitize FK path in error_response() like Conflict/Internal variants.
R-03 A05 LOW · ACCEPTED ADR-001 No RBAC enforcement — any authenticated tenant user has full read/write access regardless of role. Risk accepted. Cross-tenant isolation enforced by RLS. RBAC deferred post-P1 per docs/adr/001-defer-rbac-enforcement.md.
R-04 A06 LOW producer.rs RedpandaProducer connects without TLS or SASL authentication. Kafka traffic unencrypted. Must configure TLS and SASL before production deployment.
R-05 A06 LOW main.rs Custom migration runner silently ignores SQL errors; re-applies all migrations on every startup. Failed migration mid-run leaves schema in inconsistent state. Consider sqlx migrate or tracking applied migrations in a dedicated table.
R-06 A08 LOW templates.rs serde_json::Value accepts arbitrarily nested JSON with no recursion depth limit. 1MB body limit provides partial protection; consider explicit depth validation in domain layer.
R-07 A09 WARN Cargo.toml cargo-audit not installed — automated CVE scanning not possible. Action: cargo install cargo-audit and add cargo audit as a required CI step.
7 findings total 5 LOW · 1 ACCEPTED · 1 WARN (tooling gap)

Dependency Review

Cargo Audit Unavailable
cargo-audit is not installed in this environment — automated CVE scanning was not performed. Install with: cargo install cargo-audit && cargo audit. Add to CI/CD pipeline as a required gate before merge.
Crate Version Risk Notes
rdkafka 0.36 LOW Wraps librdkafka C library — introduces native code surface outside Rust ownership model. No known CVEs. Monitor for updates. C library memory bugs can bypass Rust safety guarantees.
lopdf 0.34 LOW Parses untrusted PDF bytes in split, merge, and rotate operations. Reduced maintenance activity. No known CVEs at time of review. Monitor for updates or consider actively-maintained alternatives.
reqwest 0.12 RESOLVED Removed as dead dependency (SEC-05). Was unused — removal reduces attack surface.
validator 0.19 RESOLVED Removed as dead dependency (SEC-05). Was unused — removal reduces attack surface.

Secrets Scan & Unsafe Code

Secrets Scan
No hardcoded secrets, API keys, or tokens found in source files
.gitignore covers .env, .env.*, *.pem, *.key — test fixtures explicitly allowed
Test RSA key at tests/fixtures/test_rsa_private.pem is tracked — test-only, never used in production
.env.example:5 contains dev credential ods-dev-2026 — intentional documentation, must not be used in staging/prod
TEST_SECRET constant in extractors.rs inside #[cfg(test)] — never compiled into production binary
Unsafe Code Audit
Zero unsafe blocks found in production source code
All .unwrap() calls on serde operations are inside #[cfg(test)] blocks only
No std::mem::transmute or raw pointer dereferences outside of tests
Rust ownership model provides full memory safety guarantees throughout all production code paths
.gitignore Assessment — Good
Covers all sensitive file patterns: .env, .env.*, *.log, *.pem, *.key. Correct exceptions for !.env.example and !tests/fixtures/*.pem. No production credentials at risk of accidental commit.