SAML Federation and SCIM Provisioning Security Review
Executive Summary
This memo documents an adversarial security review of the SAML federation (RFC-064) and SCIM provisioning (RFC-065) implementations, including the underlying proxy auth contract (RFC-063). The review covers the Rust proxy federation module, SCIM module, Go-side auth context extraction, and the integration between these layers.
Review Date: 2026-04-15
Scope: RFC-063, RFC-064, RFC-065, Rust implementation (prism-proxy/src/federation/, prism-proxy/src/scim/), Go auth context (pkg/plugin/auth_context.go), integration tests
Findings: 5 Critical, 7 High, 8 Medium, 6 Low, 6 Informational
Overall Assessment: The design specified in the RFCs is sound, but the implementation has critical gaps in several areas. The most urgent items are: (1) header stripping is not yet wired despite the helper existing FIXED, (2) SAML signature validation is a stub, (3) the Go backend trusts advisory headers without proof-bearing token validation, and (4) bearer token comparison is vulnerable to timing attacks FIXED. Remaining critical items must be resolved before any production deployment.
Remediation Progress (as of 2026-04-16): 8 of 32 findings resolved (SEC-099-C1, C3, C4, H2-partial, H3, H4, M1, M2).
Scope
In Scope
| Component | Path | RFC |
|---|---|---|
| Proxy Auth Contract | RFC-063 | Normative header set, backend tokens, header stripping |
| SAML Federation | RFC-064 | Assertion validation, canonical subjects, session bridging |
| SCIM Provisioning | RFC-065 | Provider-scoped directory, namespace bindings, deprovisioning |
| Rust Federation Module | prism-proxy/src/federation/ | Provider model, SAML validator, subject resolution |
| Rust SCIM Module | prism-proxy/src/scim/ | Resource models, store, server |
| Rust Auth Context | prism-proxy/src/auth_context.rs | Header injection, subject construction |
| Rust Backend Token | prism-proxy/src/backend_token.rs | Token claims model |
| Go Auth Context | pkg/plugin/auth_context.go | Header extraction, permission checks |
| Integration Tests | prism-proxy/tests/saml_federation_test.rs, scim_provisioning_test.rs | Coverage gaps |
Out of Scope
- Rust proxy TCP framing and HPACK handling (reviewed separately in memo-098)
- Topaz policy engine (ADR-050)
- Vault integration (memo-008, memo-084)
- Web console security (memo-097)
Methodology
Each finding is coded with a severity class, OWASP mapping, affected RFC section, affected source file, and remediation guidance. Findings reference specific RFC line numbers and source code locations.
Severity Definitions:
| Severity | Definition |
|---|---|
| CRITICAL | Direct authentication bypass, privilege escalation, or data exposure exploitable without special access |
| HIGH | Security weakness exploitable with moderate effort or specific conditions; leads to unauthorized access |
| MEDIUM | Defense-in-depth gap, misconfiguration risk, or weakness requiring controlled conditions |
| LOW | Minor hardening issue, defense-in-depth improvement, or operational hygiene |
| INFO | Design observation, architectural gap, or future consideration |
Threat Model
Attack Surfaces:
┌─────────────────────────────────────────────────────────────┐
│ Client │
│ │ Spoofed x-prism-* headers │
│ │ Forged SAML assertions │
│ │ Replay of captured SAML assertions │
│ │ SCIM injection (user/group creation) │
│ │ Timing attacks on bearer tokens │
│ │ Resource exhaustion via unbounded stores │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Rust Proxy │───>│ Go Backend │───>│ Data Store │ │
│ │ │ │ │ │ │ │
│ │ • SAML valid │ │ • Trusts hdrs│ │ • No token │ │
│ │ • No XML sig │ │ • No token │ │ validation │ │
│ │ • No strip │ │ validation │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ External IdP SCIM Endpoints Direct Access │
│ (SAML/OIDC) (Bearer Token) (No mTLS) │
└─────────────────────────────────────────────────────────────┘
Trust Boundaries:
- Client → Proxy: Untrusted. Client can send arbitrary headers, tokens, and assertions.
- Proxy → Backend: Semi-trusted. Should be verified via backend token and mTLS.
- IdP → Proxy: Trusted for identity assertions only if signature is verified.
- SCIM Provider → Prism: Trusted only if bearer token is valid and provider is registered.
CRITICAL Findings
SEC-099-C1: Header Spoofing — Client x-prism-* Headers Not Stripped
| Attribute | Value |
|---|---|
| OWASP | A01:2021 Broken Access Control |
| RFC Ref | RFC-063 §Security Gaps #1 (lines 69-78), RFC-063 §Header Stripping (lines 229-251) |
| Source | prism-proxy/src/headers.rs:18-19 (helper exists but unused), pkg/plugin/auth_context.go:67-113 (trusts all headers) |
| Status | FIXED (2026-04-16) — Header stripping wired in main.rs:117 |
Description: The inject_context_headers function in the proxy decodes client HEADERS, adds proxy-owned headers, and re-encodes. It did not strip client-supplied x-prism-* headers before injection. The helper function is_prism_header() exists in headers.rs:18-19 but is never called.
RFC-063 lines 69-78 document this vulnerability explicitly:
A client sending
x-prism-user-id: admin@evil.comin its original request will have that value survive into the re-encoded frame
The Go backend's ExtractAuthContext (auth_context.go:67-113) reads all x-prism-* headers as trusted truth with no verification.
Attack Scenario: Client sends gRPC request with x-prism-subject: oidc:dex|admin-user-id and x-prism-permission: write in the HEADERS frame. Proxy does not strip these. Backend trusts them, granting admin access.
Remediation: Wire headers::is_prism_header into the proxy's inject_context_headers as specified in RFC-063 lines 232-249:
fn inject_context_headers(original_frame, context):
headers = hpack_decode(original_frame.payload)
headers.retain(|name, _| !is_prism_header(name)) // ← ADD THIS
for (key, value) in context.to_headers():
headers.insert(key, value)
Test Gap: No test verifies that spoofed client headers are rejected. The existing header stripping tests in main.rs are a positive start but must be verified against the actual data path, not just unit tests.
SEC-099-C2: Go Backend Trusts Advisory Headers Without Backend Token Validation
| Attribute | Value |
|---|---|
| OWASP | A01:2021 Broken Access Control, A07:2021 Identification and Authentication Failures |
| RFC Ref | RFC-063 §3 Normative Header Set (lines 121-177), RFC-063 §Backend Trust Rules (lines 185-198) |
| Source | pkg/plugin/auth_context.go:67-113 |
| Status | Not implemented — ValidateBackendToken does not exist |
Description: RFC-063 defines a normative header set with one proof-bearing header (x-prism-token) and nine advisory headers. Lines 193-198 mandate:
Backends MUST validate the Prism-issued backend token (
x-prism-token) before trusting anyx-prism-*claim. Backends MUST reject requests that lack a valid backend token.
The Go ExtractAuthContext reads headers including x-prism-token but never validates it. The ValidateBackendToken function specified in RFC-063 Phase 1 (lines 458-460) is not implemented. Every x-prism-* header is treated as trusted truth.
Attack Scenario: Direct gRPC connection to backend (bypassing proxy) with crafted metadata:
metadata.NewOutgoingContext(ctx, metadata.Pairs(
"x-prism-subject", "oidc:dex|admin-user-id",
"x-prism-namespace", "team-alpha",
"x-prism-permission", "write",
"x-prism-token", "Bearer forged-token",
))
Backend extracts and trusts all values without cryptographic verification.
Remediation: Implement ValidateBackendToken in pkg/plugin/auth_context.go that verifies Ed25519-signed JWTs. Update HasPermission to require a validated backend token for write operations per RFC-063 Phase 1 line 462.
Test Gap: No Go-side test validates token rejection behavior. Integration tests should cover: missing token, invalid signature, expired token, wrong audience, mismatched subject.
SEC-099-C3: HasPermission Grants Full Access to All Unauthenticated Requests
| Attribute | Value |
|---|---|
| OWASP | A01:2021 Broken Access Control |
| RFC Ref | RFC-063 §Unauthenticated Mode (lines 618-620), RFC-063 §Phase 1 (line 446) |
| Source | pkg/plugin/auth_context.go:119-124 |
| Status | FIXED (2026-04-16) — Unauthenticated now gets Read-only in auth_context.go:119-121 |
Description: auth_context.go:119-124:
func (a *AuthContext) HasPermission(required string) bool {
if !a.IsAuthenticated {
return true // ALL unauthenticated requests get full access
}
RFC-063 line 446 specifies changing unauthenticated to Read only. Line 462 specifies requiring a valid backend token for write. Neither change is implemented.
The Rust side correctly sets Permission::Read for unauthenticated contexts (auth_context.rs:37), but the Go side ignores the permission value entirely when IsAuthenticated is false.
Attack Scenario: With auth disabled (DEX_ISSUER not set), every request to any namespace gets write permission. This is documented as "local-dev only" but there is no guard preventing this configuration in production.
Remediation:
- Remove the
return trueshort-circuit inHasPermission - Require valid backend token for write operations
- Add configuration guard that refuses to start in production mode without auth enabled
- Add test verifying unauthenticated requests get only
Readpermission
SEC-099-C4: SCIM Bearer Token Comparison Vulnerable to Timing Attack
| Attribute | Value |
|---|---|
| OWASP | A07:2021 Identification and Authentication Failures |
| RFC Ref | RFC-065 §Unauthorized Grants (lines 212-214) |
| Source | prism-proxy/src/scim/server.rs:42 |
| Status | FIXED (2026-04-16) — Uses subtle::ConstantTimeEq in server.rs:44 |
Description: server.rs:42 (now line 44):
if token != expected_token {
return Err(ScimErrorResponse::unauthenticated());
}
This uses Rust's PartialEq for str, which performs short-circuit byte comparison. An attacker measuring response times can progressively recover the bearer token character-by-character. With ~256 attempts per character position and a typical token length of 32-64 characters, full recovery takes ~16K-32K requests.
Remediation: Use constant-time comparison:
use subtle::ConstantTimeEq;
if token.as_bytes().ct_eq(expected_token.as_bytes()).unwrap_u8() == 0 {
return Err(ScimErrorResponse::unauthenticated());
}
Add subtle to Cargo.toml dev-dependencies (or use ring::constant_time).
Test Gap: No test measures timing characteristics. A statistical timing test would confirm the fix.
SEC-099-C5: No SAML Assertion Cryptographic Signature Validation
| Attribute | Value |
|---|---|
| OWASP | A02:2021 Cryptographic Failures, A07:2021 Identification and Authentication Failures |
| RFC Ref | RFC-064 §Provider Rules (lines 155-161), RFC-064 §Session Bridging (lines 117-124) |
| Source | prism-proxy/src/federation/saml.rs:110-184 |
| Status | Not implemented — InvalidSignature error variant exists but is never produced |
Description: SamlValidator::validate checks expiry, audience, recipient, replay, and NameID emptiness but never validates the assertion's cryptographic signature. The SamlError::InvalidSignature variant is defined (saml.rs:22-24) but never returned. The SamlConfig.metadata_url (saml.rs:44) stores the metadata endpoint URL but no code fetches it to retrieve signing certificates.
This is by design for local test mode (see ADR-063), but the production path requires signature validation. The current code has no abstraction point to add it — the validator would need to be refactored to accept a signature verifier trait.
Attack Scenario: Client constructs a SamlAssertion struct with arbitrary subject_name_id, attributes, and audience. The validator accepts it as long as timing checks pass. In a production deployment where assertions arrive from external IdPs, this is a complete auth bypass.
Remediation:
- Define a
SignatureVerifiertrait infederation/saml.rs - Add
SignatureVerifieras a required parameter ofSamlValidator - For local test mode, implement
NoOpSignatureVerifier(documented as insecure, test-only) - For production, implement
XmlSignatureVerifierusingopensslorxmlsec - Call
verifier.verify(&assertion)at the start ofvalidate()
Test Gap: No test covers signature validation failure path. Add test that InvalidSignature is returned when signature verification fails.
HIGH Findings
SEC-099-H1: SAML Replay Cache Grows Unboundedly, No Cross-Instance Sharing
| Attribute | Value |
|---|---|
| OWASP | A05:2021 Security Misconfiguration |
| RFC Ref | RFC-064 §Provider Rules (lines 155-161, "replay and expiry controls") |
| Source | prism-proxy/src/federation/saml.rs:113 (seen_assertion_ids: HashMap<String, u64>) |
| CVSS | 5.3 |
Description: The replay cache is a per-instance HashMap that grows until entries expire via evict_old_ids (saml.rs:168-169). Within the max_assertion_age_secs window (default 3600s), there is no bound on cache size. An attacker submitting unique assertion IDs at high volume causes memory exhaustion.
Additionally, multiple proxy instances do not share replay state. A replayed assertion that was accepted by proxy instance A will be accepted by proxy instance B.
Remediation:
- Add a maximum cache size with LRU eviction (e.g.,
lrucrate) - Implement
BackplaneBackedReplayCacheusing the Prism data plane (per RFC-063 §5) - Document the single-instance replay cache as a known limitation of local test mode
SEC-099-H2: No Input Validation on NameID, External IDs, or Subject Components
| Attribute | Value |
|---|---|
| OWASP | A03:2021 Injection |
| RFC Ref | RFC-064 §Canonical Subjects (lines 72-80), RFC-065 §Resource Model (lines 134-163) |
| Source | prism-proxy/src/federation/subject.rs:30-74, prism-proxy/src/scim/model.rs:30-43 |
| CVSS | 6.5 |
Description: Subject and SCIM identifiers accepted arbitrary strings without validation.
CanonicalSubject::saml("okta", "user|evil")now returnsErr— thevalidate_identifierfunction rejects|,:, and control charsScimUser::new("provider", "../../../etc", "user")still producesuser:scim:provider:../../../etc— SCIM model validation not yet applied (partial fix)- No length limits on any identifier component
- No restriction on control characters, null bytes, or Unicode normalization attacks
RFC-064 line 80 mandates: "Email MAY be stored as an attribute, but MUST NOT be used as the canonical identity key." The same principle should apply to all identifier components — they must be well-formed.
Remediation:
- Add a
validate_identifier(s: &str) -> Result<&str>function that rejects|,:, control chars, and enforces length limits - Apply it to
CanonicalSubjectconstructors,ScimUser::new,ScimGroup::new, andNamespaceBindingfields - Add property-based tests with arbitrary strings
SEC-099-H3: SCIM Store TOCTOU Race Conditions
| Attribute | Value |
|---|---|
| OWASP | A04:2021 Insecure Design |
| RFC Ref | RFC-065 §Idempotency (lines 168-169) |
| Source | prism-proxy/src/scim/store.rs:37-51, prism-proxy/src/scim/server.rs:52-66 |
| CVSS | 4.2 |
Description: ScimServer::create_user performs a read-then-write sequence:
get_user()to check existence (server.rs:58)put_user()to insert (server.rs:62)
Between these async operations, another task can insert the same user. The put_user method itself has a second TOCTOU: ensure_provider() acquires and releases the write lock, then put_user acquires it again.
RFC-065 §Idempotency (lines 168-169): "SCIM operations MUST be idempotent at the provider-object level."
Remediation: Refactor store operations to hold a single write lock for the entire check-then-mutate sequence, or use an upsert pattern.
SEC-099-H4: ScimUser Accepts Arbitrary Fields via Serde Flatten
| Attribute | Value |
|---|---|
| OWASP | A08:2021 Software and Data Integrity Failures |
| RFC Ref | RFC-065 §Resource Model (lines 134-163) |
| Source | prism-proxy/src/scim/model.rs:26-27 |
| CVSS | 5.3 |
Description: model.rs:26-27:
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
This accepts any JSON fields without size limits. An attacker can inject:
- Thousands of keys (memory exhaustion)
- Multi-MB values (store corruption)
- Override fields like
schemas,id, ormetaduring deserialization of attacker-controlled input (schema injection)
Remediation:
- Add a max size for the
extramap (e.g., 64 keys, 4KB total) - Use
#[serde(deny_unknown_fields)]or validate thatextrakeys do not overlap with canonical fields - Add a
validate()method onScimUserthat enforces constraints
SEC-099-H5: SCIM Server Provider ID Trusted Without Whitelist Verification
| Attribute | Value |
|---|---|
| OWASP | A01:2021 Broken Access Control |
| RFC Ref | RFC-065 §SCIM Writes (lines 67-84) |
| Source | prism-proxy/src/scim/server.rs:16 |
| CVSS | 5.4 |
Description: ScimServer is constructed with a provider_id string that is used directly in all storage operations. The provider_id is never validated against a whitelist of configured/registered providers. If the HTTP routing layer maps requests to the wrong ScimServer instance, operations affect the wrong provider's directory.
RFC-065 lines 69-75 define provider identities like scim:okta-enterprise. These should be registered and verified.
Remediation: Maintain a registry of approved provider IDs in FederationConfig. Validate that the ScimServer's provider_id matches a registered provider before allowing mutations.
SEC-099-H6: JWT Validator Fetches Only First JWKS Key, No Rotation Support
| Attribute | Value |
|---|---|
| OWASP | A02:2021 Cryptographic Failures |
| RFC Ref | RFC-064 §OIDC providers (lines 148-153) |
| Source | prism-proxy/src/auth.rs:110-117 |
| CVSS | 4.8 |
Description: auth.rs:110-117:
let key = keys
.first()
.ok_or_else(|| AuthError::JwksFetchFailed("Empty JWKS".to_string()))?;
Only the first key from the JWKS endpoint is used. During key rotation, the IdP typically lists the new key first, making old tokens unvalidated. Or if the old key is listed first, new tokens are rejected. RFC-064 lines 148-153 require "JWKS source" and "clock skew tolerance" for OIDC providers.
The JWKS fetch also uses plain HTTP reqwest::get (auth.rs:101) which is vulnerable to MITM if the issuer URL uses HTTP instead of HTTPS.
Remediation: Iterate all JWKS keys, trying each until one succeeds. Cache the full key set and refresh periodically.
SEC-099-H7: Backend Token Audience Validation Disabled
| Attribute | Value |
|---|---|
| OWASP | A02:2021 Cryptographic Failures |
| RFC Ref | RFC-063 §Backend Token Claims (lines 200-215), RFC-063 §Open Question #5 (lines 635-636) |
| Source | prism-proxy/src/backend_token.rs (claims model) |
| CVSS | 5.0 |
Description: The BackendTokenClaims struct defines an aud field per RFC-063 lines 200-215, but the actual token validation code (when implemented) must not disable audience checking. RFC-063 line 215 specifies:
The
audclaim is scoped to the namespace and backend type (e.g.,keyvalue/team-alpha). This restricts token reuse across backends.
Currently the token mint/validate functions are stubs. The existing auth.rs:163 sets validation.set_audience(&[&self.client_id]) for OIDC tokens, but backend token validation (Phase 2) must enforce audience matching.
Remediation: When implementing validate_token for backend tokens, enforce audience validation. Do not repeat the pattern of disabling it.
MEDIUM Findings
SEC-099-M1: slugify_issuer Produces Collisions Between Different IdPs
| Attribute | Value |
|---|---|
| RFC Ref | RFC-064 §Canonical Subjects (lines 72-80) |
| Source | prism-proxy/src/auth_context.rs:94-103 |
Description: slugify_issuer("https://dex.example.com") → "dex", slugify_issuer("https://dex.attack.com") → "dex". Two unrelated IdPs produce the same issuer slug, causing subject identifier collisions: oidc:dex|user123 from two different IdPs would be treated as the same subject.
RFC-064 line 93 warns: "If the same human authenticates through two providers and no explicit link exists, Prism MUST treat those as two distinct subjects."
Remediation: Use the full hostname (not just first component) or a hash of the issuer URL as the slug.
SEC-099-M2: SAML Recipient Validation Is Optional and Skipped When None
| Attribute | Value |
|---|---|
| RFC Ref | RFC-064 §Provider Rules (lines 155-161) |
| Source | prism-proxy/src/federation/saml.rs:145-154 |
Description: When SamlConfig.recipient is None, the assertion's recipient is not validated. A default-constructed config with no recipient set accepts assertions targeted at any endpoint. RFC-064 lines 159-160 mandate "audience and recipient validation rules."
Remediation: Make recipient a required field in SamlConfig (non-Option). For local test mode, require explicit opt-out.
SEC-099-M3: SCIM Delete Is Hard Delete, Not Lifecycle Transition
| Attribute | Value |
|---|---|
| RFC Ref | RFC-065 §Deprovisioning (lines 99-108) |
| Source | prism-proxy/src/scim/store.rs:73-87, prism-proxy/src/scim/server.rs:100-105 |
Description: ScimStore::delete_user performs a hard remove from the HashMap. RFC-065 line 108 mandates: "Deletion MUST be handled as a lifecycle transition, not an unsafe immediate hard-delete of shared authorization state." No audit trail, no binding reconciliation, no soft-delete marker.
Remediation: Replace hard delete with a deleted_at: Option<u64> field. Implement a garbage collection pass that cleans up after a grace period.
SEC-099-M4: reconcile_deprovisioned Reports But Does Not Remove Memberships
| Attribute | Value |
|---|---|
| RFC Ref | RFC-065 §Deprovisioning (lines 99-108, step 3) |
| Source | prism-proxy/src/scim/store.rs:264-289 |
Description: reconcile_deprovisioned returns affected group memberships but does not remove them. RFC-065 line 105: "derived access from group membership is removed through reconciliation." A deprovisioned user retains group memberships and derived namespace access.
Remediation: Add reconcile_deprovisioned_and_remove that returns affected memberships and removes them.
SEC-099-M5: SCIM Patch Replace Operation Is a Silent No-Op
| Attribute | Value |
|---|---|
| RFC Ref | RFC-065 §Sync Semantics (lines 168-169) |
| Source | prism-proxy/src/scim/server.rs:202 |
Description: PatchOpType::Replace => {} does nothing silently. A misconfigured IdP sending Replace operations to change active status (deprovisioning) gets 200 OK with no change applied. This violates SCIM spec and could mask deprovisioning failures.
Remediation: Implement Replace for critical fields (active, displayName, userName). At minimum, return an error for unsupported operations.
SEC-099-M6: No Rate Limiting on SCIM or SAML Endpoints
| Attribute | Value |
|---|---|
| RFC Ref | RFC-065 §Blast Radius (lines 206-208) |
| Source | prism-proxy/src/scim/server.rs, prism-proxy/src/federation/saml.rs |
Description: Neither SCIM nor SAML endpoints have rate limiting. Combined with SEC-099-H1 (unbounded replay cache) and SEC-099-H4 (unbounded extra fields), resource exhaustion attacks are feasible.
Remediation: Add configurable rate limiting per provider and per endpoint. Consider governor crate or token bucket.
SEC-099-M7: Namespace Policy Uses Linear Search with Timing Oracle
| Attribute | Value |
|---|---|
| RFC Ref | RFC-063 §Namespace Security Model (lines 333-342) |
| Source | prism-proxy/src/auth.rs:66-74 |
Description: NamespacePolicy::has_permission uses Vec::contains (O(n) linear search). For large user lists, comparison time varies by list size and match position, creating a timing side-channel that reveals user membership.
Remediation: Use HashSet<String> instead of Vec<String> for user lists.
SEC-099-M8: SAML issued_at Falls Back to Unix Epoch Zero
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/federation/saml.rs:240-246 |
Description: When authn_statement is None, issued_at becomes 0 (January 1, 1970). This is not validated against, allowing assertions with no issuance timestamp. Combined with clock skew tolerance, assertions with issued_at: 0 could pass validation.
Remediation: Require issued_at to be within the clock skew window of the current time, or reject assertions with issued_at == 0.
LOW Findings
SEC-099-L1: SCIM Bearer Token Stored as Plaintext in Memory
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/config/mod.rs:46, prism-proxy/src/scim/server.rs:16 |
Description: The bearer token is stored as a String in ScimServer and ScimConfig. No zeroing on drop, no Zeroize derive, no secret masking in debug output. Memory dumps or log output could expose the token.
Remediation: Use zeroize::Zeroize for the token field. Implement Drop that zeroes memory. Mask in Debug output.
SEC-099-L2: No Namespace Format Validation — Reserved Names Injected
| Attribute | Value |
|---|---|
| RFC Ref | RFC-063 §Proxy Backplane (lines 277-279) |
| Source | prism-proxy/src/auth_context.rs:24, prism-proxy/src/config/mod.rs |
Description: Namespace strings are accepted without format validation. RFC-063 line 278 reserves __prism_system for the proxy backplane. An attacker (or misconfigured client) could use __prism_system as a namespace, potentially interfering with backplane operations. Path-traversal-like strings (../../etc) are also unvalidated.
Remediation: Add namespace format validation: alphanumeric + hyphens, no double underscores prefix, max length.
SEC-099-L3: BackplaneClient::get_signing_key Returns Raw Bytes
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/backplane.rs:11 |
Description: When implemented, signing key material will be returned as raw Vec<u8> with no encryption-at-rest, no access logging, and no wrapping. The current todo!() panics if called.
Remediation: Design the backplane key management to use hardware-backed key stores or at minimum envelope encryption.
SEC-099-L4: SCIM Store Double Write Lock Acquisition
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/scim/store.rs:27-35, 38-39 |
Description: put_user calls ensure_provider (acquires write lock, releases it), then acquires write lock again. Between acquisitions, another task could delete the provider directory.
Remediation: Refactor to hold a single lock acquisition for the entire operation.
SEC-099-L5: SAML Issuer Not Validated Against Configured IdP
| Attribute | Value |
|---|---|
| RFC Ref | RFC-064 §Provider Rules (lines 155-161, "metadata source") |
| Source | prism-proxy/src/federation/saml.rs:110-184 |
Description: The SamlValidator does not check that the assertion's issuer field matches the expected issuer from SamlConfig.metadata_url. Any issuer string is accepted.
Remediation: Add issuer validation at the start of validate(): reject if assertion.issuer does not match the configured issuer for this validator.
SEC-099-L6: Test Policy Hardcoded User IDs in Production Code Path
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/auth.rs:206-209 |
Description: AuthorizationService::with_test_policies() contains hardcoded Dex user IDs in a function that is part of the production code module. If called in production (via misconfiguration), it creates known-good authz entries.
Remediation: Gate with_test_policies behind #[cfg(test)] or a feature flag.
INFORMATIONAL Findings
SEC-099-I1: No Cryptographic Binding Between SAML Subject and SCIM Identity
| Attribute | Value |
|---|---|
| RFC Ref | RFC-065 §Resource Model (lines 134-163, "optional canonical-subject binding reference"), RFC-064 §Canonical Subjects (lines 72-80) |
Description: SAML produces saml:corp-okta|alice@example.com subjects. SCIM produces user:scim:corp-okta:2819c223 identities. RFC-065 line 84 mandates: "that relationship MUST be represented as an explicit binding." This binding is not implemented. Without it, a SCIM-provisioned user has no verifiable link to their SAML login identity.
SEC-099-I2: No Audit Logging in Any Implementation
| Attribute | Value |
|---|---|
| RFC Ref | RFC-063 §6 Namespace Security Model (lines 333-342), RFC-064 §Provider Drift (lines 206-208) |
Description: RFC-064 lines 206-208 mandate: "Audit logs MUST record which external provider produced each Prism session." RFC-063 §6 defines "audit requirements" per namespace. No audit logging exists in any implementation file.
SEC-099-I3: Federation Config Defaults to Disabled (Safe Default)
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/federation/provider.rs:49-53 |
Description: FederationConfig defaults to empty providers and LinkMode::Disabled. This is a safe default — a misconfigured deployment silently accepts no federation rather than accepting all.
SEC-099-I4: SCIM extra Field Vulnerable to Schema Override via Flatten
| Attribute | Value |
|---|---|
| Source | prism-proxy/src/scim/model.rs:26 |
Description: The #[serde(flatten)] on extra means an attacker could inject schemas, id, or meta fields that override the canonical ones during deserialization of attacker-controlled JSON. Serde's flatten behavior for overlapping keys is implementation-defined.
SEC-099-I5: No Typed Constructor for SAML-Derived AuthContext
| Attribute | Value |
|---|---|
| Source | prism-proxy/tests/saml_federation_test.rs:85-102 |
Description: Tests construct AuthContext::unauthenticated() then overwrite fields for SAML-derived contexts. Production code should have a AuthContext::from_saml(subject, issuer, namespace, permission) constructor to prevent forgetting required fields.
SEC-099-I6: RFC-063 Open Question #4 — Backplane Bootstrapping Race Unresolved
| Attribute | Value |
|---|---|
| RFC Ref | RFC-063 §Open Questions (lines 634-635) |
Description: Multiple proxies starting simultaneously against an empty system namespace will race to create the signing key. This is acknowledged but unresolved. A distributed lock or designated primary is needed.
Findings Summary
By Severity
| Severity | ID | Title | RFC Section |
|---|---|---|---|
| CRITICAL | SEC-099-C1 | Header spoofing — no stripping | RFC-063 §Security Gaps #1 |
| CRITICAL | SEC-099-C2 | Go backend trusts advisory headers | RFC-063 §3 |
| CRITICAL | SEC-099-C3 | Unauthenticated gets full access | RFC-063 §Unauthenticated Mode |
| CRITICAL | SEC-099-C4 | SCIM bearer token timing attack | RFC-065 §Unauthorized Grants |
| CRITICAL | SEC-099-C5 | No SAML signature validation | RFC-064 §Provider Rules |
| HIGH | SEC-099-H1 | Unbounded SAML replay cache | RFC-064 §Provider Rules |
| HIGH | SEC-099-H2 | No input validation on identifiers | RFC-064 §Canonical Subjects, RFC-065 §Resource Model |
| HIGH | SEC-099-H3 | SCIM store TOCTOU races | RFC-065 §Idempotency |
| HIGH | SEC-099-H4 | Unbounded ScimUser extra fields | RFC-065 §Resource Model |
| HIGH | SEC-099-H5 | Provider ID not whitelisted | RFC-065 §SCIM Writes |
| HIGH | SEC-099-H6 | First-key-only JWKS fetch | RFC-064 §OIDC Providers |
| HIGH | SEC-099-H7 | Backend token audience disabled | RFC-063 §Backend Token Claims |
| MEDIUM | SEC-099-M1 | Issuer slug collisions | RFC-064 §Canonical Subjects |
| MEDIUM | SEC-099-M2 | Optional recipient validation | RFC-064 §Provider Rules |
| MEDIUM | SEC-099-M3 | Hard delete, not lifecycle | RFC-065 §Deprovisioning |
| MEDIUM | SEC-099-M4 | Reconciliation reports only | RFC-065 §Deprovisioning |
| MEDIUM | SEC-099-M5 | SCIM Replace is no-op | RFC-065 §Sync Semantics |
| MEDIUM | SEC-099-M6 | No rate limiting | RFC-065 §Blast Radius |
| MEDIUM | SEC-099-M7 | Linear search timing oracle | RFC-063 §Namespace Security |
| MEDIUM | SEC-099-M8 | issued_at falls back to epoch 0 | — |
| LOW | SEC-099-L1 | Bearer token plaintext in memory | — |
| LOW | SEC-099-L2 | No namespace format validation | RFC-063 §Proxy Backplane |
| LOW | SEC-099-L3 | Raw key bytes from backplane | — |
| LOW | SEC-099-L4 | Double write lock acquisition | — |
| LOW | SEC-099-L5 | SAML issuer not validated | RFC-064 §Provider Rules |
| LOW | SEC-099-L6 | Test policies in production path | — |
| INFO | SEC-099-I1 | No SAML-SCIM identity binding | RFC-064 §Canonical Subjects, RFC-065 §Resource Model |
| INFO | SEC-099-I2 | No audit logging | RFC-063 §6, RFC-064 §Provider Drift |
| INFO | SEC-099-I3 | Safe defaults (positive) | — |
| INFO | SEC-099-I4 | Serde flatten schema override | — |
| INFO | SEC-099-I5 | No typed SAML AuthContext constructor | — |
| INFO | SEC-099-I6 | Backplane bootstrapping race | RFC-063 §Open Questions |
By OWASP Category
| OWASP | Findings |
|---|---|
| A01: Broken Access Control | C1, C2, C3, H5 |
| A02: Cryptographic Failures | C5, H6, H7 |
| A03: Injection | H2 |
| A04: Insecure Design | H3 |
| A05: Security Misconfiguration | H1 |
| A07: Identification and Auth Failures | C2, C4, C5 |
| A08: Software and Data Integrity | H4 |
By RFC
| RFC | Critical | High | Medium+Low+Info | Total |
|---|---|---|---|---|
| RFC-063 (Proxy Auth Contract) | 3 | 1 | 5 | 9 |
| RFC-064 (SAML Federation) | 1 | 3 | 3 | 7 |
| RFC-065 (SCIM Provisioning) | 1 | 3 | 5 | 9 |
| Implementation (no RFC gap) | 0 | 0 | 3 | 3 |
Remediation Priority
Phase 1: Immediate (Blocks any production deployment)
- SEC-099-C1: Wire header stripping (helper exists, ~5 lines)
- SEC-099-C4: Constant-time bearer token comparison (~3 lines)
- SEC-099-C3: Remove
return truein GoHasPermission(~2 lines)
Phase 2: Required Before Federation Ships
- SEC-099-C5: Add
SignatureVerifiertrait to SAML validator - SEC-099-C2: Implement
ValidateBackendTokenin Go - SEC-099-H2: Add identifier validation functions
- SEC-099-M1: Fix
slugify_issuerto use full hostname
Phase 3: Hardening
- SEC-099-H1: Bound SAML replay cache, add LRU eviction
- SEC-099-H3: Refactor SCIM store to single-lock operations
- SEC-099-H4: Cap
extrafield size - SEC-099-M2: Make SAML recipient required
- SEC-099-M3/M4: Implement lifecycle transitions and active reconciliation
- SEC-099-H6: Iterate all JWKS keys
- SEC-099-I1/I2: Implement SAML-SCIM binding and audit logging
Related Documents
- RFC-063: Normative Proxy Auth Contract
- RFC-064: Federation Profile
- RFC-065: SCIM Provisioning
- ADR-063: SAML Federation Local Test Mode
- ADR-064: SCIM Provisioning Local Test Mode
- MEMO-097: Prism Web Console Security Review
- MEMO-098: Prism Proxy Security Requirements
Revision History
- 2026-04-15: Initial security review — 32 findings (5 Critical, 7 High, 8 Medium, 6 Low, 6 Info)