Security Review · ODS Platform

OID — Security Audit

2026-03-19 commit 9f512d5 OWASP Top 10
⛔ CRITICAL
OWASP Score  6 /10
1
Critical
7
High
7
Medium
5
Low
0
Secrets
0
Unsafe
A01
Injection
✓ Pass
All SQL parameterized via sqlx. Atomic UPDATE…RETURNING. HTTPS-only redirect URIs.
A02
Broken Authentication
⚠ Warn
No rate limiting on login. Ephemeral RSA keys invalidated on restart.
A03
Sensitive Data Exposure
⚠ Warn
e.to_string() leaks DB internals in error responses across 9 call sites.
A04
XXE
— N/A
JSON-only API. No XML parsing present.
A05
Broken Access Control
✕ Fail
CRITICAL: RLS bypass. All mgmt endpoints unauthenticated. IDOR on client listing.
A06
Security Misconfiguration
⚠ Warn
CORS not wired despite actix-cors in Cargo.toml. No body size limits.
A07
Cross-Site Scripting
— N/A
JSON-only API. No HTML rendering.
A08
Insecure Deserialization
✓ Pass
Typed serde structs. PKCE verifier length and char bounds enforced.
A09
Known Vulnerabilities
⚠ Warn
cargo-audit absent from CI. Hand-rolled SHA-256. librdkafka needs CVE monitoring.
A10
Insufficient Logging
✓ Pass
CloudEvents audit trail. TracingLogger with correlation IDs. Auth events emitted.
Finding Severity Distribution
Critical
1
High
7
Medium
7
Low
5
Merge Blocker: 1 CRITICAL finding and 7 HIGH findings are present. The service must not be deployed to staging or production until the RLS bypass and unauthenticated management endpoints are resolved.
Critical RLS Tenant Isolation Bypass
A05 · Broken Access Control
PostgreSQL Row-Level Security is defined on all 6 tables and relies on current_setting('app.tenant_id', true) being set per-request. The production application code never executes SET LOCAL app.tenant_id before any query. In production, all RLS SELECT policies evaluate to NULL::UUID = current_setting(...) which is permissive — all tenant data is accessible across tenant boundaries. This is a complete multi-tenant data isolation failure. The session variable is only set in integration tests.
File: src/repository/user_repo.rs:20
Also: migrations/005_enable_rls_users_roles.sql
High All Management Endpoints Unauthenticated
A05 · Broken Access Control
No AuthenticatedUser extractor applied to any management endpoint. Any unauthenticated caller can create users, read user data, assign roles, register OAuth clients, or create tenants.
POST /v1/users GET /v1/users/{id} PATCH /v1/users/{id} PUT /v1/users/{id}/roles — src/api/users.rs:69
POST /v1/tenants GET /v1/tenants/{id} — src/api/tenants.rs:54
POST /v1/clients GET /v1/clients — src/api/clients.rs:89
POST /v1/roles GET /v1/roles — src/api/roles.rs:1
High No Rate Limiting on Login Endpoint
A02 · Broken Authentication
POST /v1/auth/login has no rate limiting, account lockout, CAPTCHA, or exponential backoff. An attacker can attempt unlimited password guesses without any throttling. This directly enables brute-force credential attacks against any known email address.
File: src/main.rs:166
Handler: src/api/auth.rs
High CORS Not Configured
A06 · Security Misconfiguration
actix-cors is listed in Cargo.toml but is never imported or applied as middleware. No Cors middleware is registered in App::new(). For an OIDC identity service called by browser clients, this means no CORS headers are sent — legitimate cross-origin calls from SPAs are blocked, and no CORS security is enforced.
File: src/main.rs:133
High user_roles Table RLS Policy Is ALLOW ALL
A05 · Broken Access Control
The user_roles junction table has an RLS policy of USING (true). There is zero tenant isolation at the database layer for role assignments. Any database connection can read or write role assignments across all tenants.
File: migrations/005_enable_rls_users_roles.sql:36
High IDOR — Caller-Supplied tenant_id Not Validated Against JWT
A05 · Broken Access Control
GET /v1/clients?tenant_id={uuid} accepts an arbitrary tenant_id query parameter. Even after authentication is added, the caller's JWT tenant_id claim is never cross-validated against the requested tenant, enabling Insecure Direct Object Reference access across tenant boundaries.
File: src/api/clients.rs:147
High Internal Error Details Leaked in API Responses
A03 · Sensitive Data Exposure
Multiple InternalServerError responses include e.to_string() in the JSON message field. This leaks database constraint names, table names, column names, and query fragments to API clients. Affects 9+ call sites across 3 files.
Files: src/api/users.rs:107,145,169,202,233,251
src/api/tenants.rs:84,112
src/api/clients.rs:126,155
High Ephemeral RSA Keys — All JWTs Invalidated on Restart
A02 · Broken Authentication
RSA-2048 key pair is generated fresh on every service startup. All previously issued JWTs become invalid after any restart. In a multi-instance deployment, each instance will reject the other's tokens. No key persistence or JWKS rotation strategy is implemented.
File: src/main.rs:67
Severity Category Description Location
Medium A09 Hand-rolled SHA-256 (233 lines) instead of sha2 crate. Violates "no custom crypto" principle. Functionally verified via RFC 7636 vectors but not independently auditable.
Replace with sha2 = "0.10" from RustCrypto.
src/domain/auth_code.rs:116
Medium A06 No explicit request body size limits via JsonConfig or PayloadConfig. actix-web defaults to 256KB but no explicit enforcement. src/main.rs
Medium A09 cargo-audit not installed. Automated dependency vulnerability scanning absent from CI pipeline. Cargo.toml
Medium Deps actix-cors declared as a dependency but never imported or wired. Must be activated with proper CORS config (see HIGH finding above). Cargo.toml:9
Medium A10 login.failed event includes raw error reason string. Internal DB errors would expose their message in the Redpanda event payload to all event consumers. src/api/auth.rs:55
Low A03 login.succeeded and user.created events include email addresses in plaintext in Redpanda payload. Ensure topic ACLs restrict PII access. src/events/producer.rs:108
Low A09 rdkafka with cmake-build links librdkafka natively. librdkafka has historical CVEs — requires cargo audit monitoring in CI. Cargo.toml:20
Low A06 InMemoryProducer fallback silently activates when REDPANDA_BROKERS is unset. No guard prevents accidental production use. src/main.rs:119
Low Deps validator crate declared but never used. Manual validation implemented instead. Dead dependency increases attack surface unnecessarily. Cargo.toml:28
Low A10 Internal DB errors in login.failed events could expose system state to downstream Redpanda consumers with topic read access. src/api/auth.rs:55
All SQL uses parameterized queries via sqlx — zero string interpolation
JWT validated with RS256 — signature, issuer, audience, and expiry all enforced
Argon2 password hashing with random salt via OsRng — no weak hashes
PKCE mandatory — S256 only, plain method explicitly rejected per RFC 7636
Single-use auth codes with 600s TTL — replay attacks rejected atomically via UPDATE…RETURNING
Refresh token rotation on use — old token revoked before new one is issued
No hardcoded secrets — .gitignore covers .env, *.pem, *.key, *.crt, *.p12
CloudEvents audit trail — login.succeeded, login.failed emitted to Redpanda
TracingLogger middleware with correlation IDs on all HTTP requests
Non-root Docker container — multi-stage build, user 'oid' uid 1000
Redirect URI enforces HTTPS-only (or localhost) — blocks open-redirect via http://
Login returns generic error for both NotFound and Unauthorized — no user enumeration
1
Fix RLS — Set app.tenant_id on every database transaction
Implement a middleware or repository wrapper that executes SET LOCAL app.tenant_id = $1 at the start of each transaction, sourced from the JWT tenant_id claim. Also fix the ALLOW ALL policy on user_roles to use a tenant-scoped join against the users table.
Critical
2
Add AuthenticatedUser extractor to all management endpoints
Apply the JWT extractor to /v1/users, /v1/tenants, /v1/clients, and /v1/roles. Validate the JWT tenant_id against the requested resource's tenant on every operation. Add RBAC role checks for privileged operations (admin, service-account).
High
3
Implement rate limiting on POST /v1/auth/login
Add actix-governor or a token bucket middleware keyed on IP address and/or email. Recommended: 5 attempts/minute per IP, 10 attempts/hour per email, with exponential backoff. Log and emit events on threshold breach.
High
4
Wire actix-cors middleware with explicit allowed origins
Import and register Cors middleware in App::new(). Read allowed origins from an env var (CORS_ALLOWED_ORIGINS). Use Cors::default() as a starting point then restrict — never use permissive() in production.
High
5
Persist RSA key pair — load from env or secret manager at startup
Read the RSA private key from a JWT_PRIVATE_KEY env var (PEM base64-encoded) at startup. Fall back to generating and logging a warning in dev only. Expose the public JWKS endpoint for token validation across multiple instances.
High
6
Replace e.to_string() in error responses with safe generic messages
Return a static "Internal server error" JSON message from all InternalServerError paths. Log the actual error via tracing::error! with the request correlation ID. Validate the tenant_id query param against the JWT claim in GET /v1/clients.
High
7
Replace hand-rolled SHA-256 with sha2 crate
Add sha2 = "0.10" to Cargo.toml. Replace the 233-line custom implementation in auth_code.rs with Sha256::digest(). Remove unused validator crate dependency.
Medium
8
Add cargo-audit to CI — fail on critical/high advisories
Add a cargo audit --deny warnings step as a required CI check on all PRs. This automatically catches librdkafka CVEs and any future vulnerable crate versions. Consider cargo deny for license and duplicate detection too.
Medium
Dependency Review — Manual Only
cargo-audit not available
cargo-audit binary is not installed in this environment. No CVEs were identified by manual inspection, but automated scanning must be added to CI before this service can be considered production-safe.
CrateVersionPurposeStatus
actix-web4HTTP frameworkCurrent major version
sqlx0.8DatabaseCurrent, parameterized queries
argon20.5Password hashingRustCrypto, sound
jsonwebtoken9JWT validationCurrent
rsa0.9RS256 signingRustCrypto, current
rand0.9CSPRNGCurrent
rdkafkaRedpanda clientLinks librdkafka natively — monitor CVEs in CI
actix-corsCORS (unused)Declared but never wired — configure or remove
validatorValidation (unused)Dead dependency — remove to reduce surface area