Skip to main content

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

ComponentPathRFC
Proxy Auth ContractRFC-063Normative header set, backend tokens, header stripping
SAML FederationRFC-064Assertion validation, canonical subjects, session bridging
SCIM ProvisioningRFC-065Provider-scoped directory, namespace bindings, deprovisioning
Rust Federation Moduleprism-proxy/src/federation/Provider model, SAML validator, subject resolution
Rust SCIM Moduleprism-proxy/src/scim/Resource models, store, server
Rust Auth Contextprism-proxy/src/auth_context.rsHeader injection, subject construction
Rust Backend Tokenprism-proxy/src/backend_token.rsToken claims model
Go Auth Contextpkg/plugin/auth_context.goHeader extraction, permission checks
Integration Testsprism-proxy/tests/saml_federation_test.rs, scim_provisioning_test.rsCoverage 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:

SeverityDefinition
CRITICALDirect authentication bypass, privilege escalation, or data exposure exploitable without special access
HIGHSecurity weakness exploitable with moderate effort or specific conditions; leads to unauthorized access
MEDIUMDefense-in-depth gap, misconfiguration risk, or weakness requiring controlled conditions
LOWMinor hardening issue, defense-in-depth improvement, or operational hygiene
INFODesign 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:

  1. Client → Proxy: Untrusted. Client can send arbitrary headers, tokens, and assertions.
  2. Proxy → Backend: Semi-trusted. Should be verified via backend token and mTLS.
  3. IdP → Proxy: Trusted for identity assertions only if signature is verified.
  4. 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

AttributeValue
OWASPA01:2021 Broken Access Control
RFC RefRFC-063 §Security Gaps #1 (lines 69-78), RFC-063 §Header Stripping (lines 229-251)
Sourceprism-proxy/src/headers.rs:18-19 (helper exists but unused), pkg/plugin/auth_context.go:67-113 (trusts all headers)
StatusFIXED (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.com in 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

AttributeValue
OWASPA01:2021 Broken Access Control, A07:2021 Identification and Authentication Failures
RFC RefRFC-063 §3 Normative Header Set (lines 121-177), RFC-063 §Backend Trust Rules (lines 185-198)
Sourcepkg/plugin/auth_context.go:67-113
StatusNot implementedValidateBackendToken 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 any x-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

AttributeValue
OWASPA01:2021 Broken Access Control
RFC RefRFC-063 §Unauthenticated Mode (lines 618-620), RFC-063 §Phase 1 (line 446)
Sourcepkg/plugin/auth_context.go:119-124
StatusFIXED (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:

  1. Remove the return true short-circuit in HasPermission
  2. Require valid backend token for write operations
  3. Add configuration guard that refuses to start in production mode without auth enabled
  4. Add test verifying unauthenticated requests get only Read permission

SEC-099-C4: SCIM Bearer Token Comparison Vulnerable to Timing Attack

AttributeValue
OWASPA07:2021 Identification and Authentication Failures
RFC RefRFC-065 §Unauthorized Grants (lines 212-214)
Sourceprism-proxy/src/scim/server.rs:42
StatusFIXED (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

AttributeValue
OWASPA02:2021 Cryptographic Failures, A07:2021 Identification and Authentication Failures
RFC RefRFC-064 §Provider Rules (lines 155-161), RFC-064 §Session Bridging (lines 117-124)
Sourceprism-proxy/src/federation/saml.rs:110-184
StatusNot implementedInvalidSignature 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:

  1. Define a SignatureVerifier trait in federation/saml.rs
  2. Add SignatureVerifier as a required parameter of SamlValidator
  3. For local test mode, implement NoOpSignatureVerifier (documented as insecure, test-only)
  4. For production, implement XmlSignatureVerifier using openssl or xmlsec
  5. Call verifier.verify(&assertion) at the start of validate()

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

AttributeValue
OWASPA05:2021 Security Misconfiguration
RFC RefRFC-064 §Provider Rules (lines 155-161, "replay and expiry controls")
Sourceprism-proxy/src/federation/saml.rs:113 (seen_assertion_ids: HashMap<String, u64>)
CVSS5.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:

  1. Add a maximum cache size with LRU eviction (e.g., lru crate)
  2. Implement BackplaneBackedReplayCache using the Prism data plane (per RFC-063 §5)
  3. 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

AttributeValue
OWASPA03:2021 Injection
RFC RefRFC-064 §Canonical Subjects (lines 72-80), RFC-065 §Resource Model (lines 134-163)
Sourceprism-proxy/src/federation/subject.rs:30-74, prism-proxy/src/scim/model.rs:30-43
CVSS6.5

Description: Subject and SCIM identifiers accepted arbitrary strings without validation.

  • CanonicalSubject::saml("okta", "user|evil") now returns Err — the validate_identifier function rejects |, :, and control chars
  • ScimUser::new("provider", "../../../etc", "user") still produces user: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:

  1. Add a validate_identifier(s: &str) -> Result<&str> function that rejects |, :, control chars, and enforces length limits
  2. Apply it to CanonicalSubject constructors, ScimUser::new, ScimGroup::new, and NamespaceBinding fields
  3. Add property-based tests with arbitrary strings

SEC-099-H3: SCIM Store TOCTOU Race Conditions

AttributeValue
OWASPA04:2021 Insecure Design
RFC RefRFC-065 §Idempotency (lines 168-169)
Sourceprism-proxy/src/scim/store.rs:37-51, prism-proxy/src/scim/server.rs:52-66
CVSS4.2

Description: ScimServer::create_user performs a read-then-write sequence:

  1. get_user() to check existence (server.rs:58)
  2. 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

AttributeValue
OWASPA08:2021 Software and Data Integrity Failures
RFC RefRFC-065 §Resource Model (lines 134-163)
Sourceprism-proxy/src/scim/model.rs:26-27
CVSS5.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, or meta during deserialization of attacker-controlled input (schema injection)

Remediation:

  1. Add a max size for the extra map (e.g., 64 keys, 4KB total)
  2. Use #[serde(deny_unknown_fields)] or validate that extra keys do not overlap with canonical fields
  3. Add a validate() method on ScimUser that enforces constraints

SEC-099-H5: SCIM Server Provider ID Trusted Without Whitelist Verification

AttributeValue
OWASPA01:2021 Broken Access Control
RFC RefRFC-065 §SCIM Writes (lines 67-84)
Sourceprism-proxy/src/scim/server.rs:16
CVSS5.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

AttributeValue
OWASPA02:2021 Cryptographic Failures
RFC RefRFC-064 §OIDC providers (lines 148-153)
Sourceprism-proxy/src/auth.rs:110-117
CVSS4.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

AttributeValue
OWASPA02:2021 Cryptographic Failures
RFC RefRFC-063 §Backend Token Claims (lines 200-215), RFC-063 §Open Question #5 (lines 635-636)
Sourceprism-proxy/src/backend_token.rs (claims model)
CVSS5.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 aud claim 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

AttributeValue
RFC RefRFC-064 §Canonical Subjects (lines 72-80)
Sourceprism-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

AttributeValue
RFC RefRFC-064 §Provider Rules (lines 155-161)
Sourceprism-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

AttributeValue
RFC RefRFC-065 §Deprovisioning (lines 99-108)
Sourceprism-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

AttributeValue
RFC RefRFC-065 §Deprovisioning (lines 99-108, step 3)
Sourceprism-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

AttributeValue
RFC RefRFC-065 §Sync Semantics (lines 168-169)
Sourceprism-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

AttributeValue
RFC RefRFC-065 §Blast Radius (lines 206-208)
Sourceprism-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

AttributeValue
RFC RefRFC-063 §Namespace Security Model (lines 333-342)
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
RFC RefRFC-063 §Proxy Backplane (lines 277-279)
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
RFC RefRFC-064 §Provider Rules (lines 155-161, "metadata source")
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
RFC RefRFC-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

AttributeValue
RFC RefRFC-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)

AttributeValue
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
Sourceprism-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

AttributeValue
RFC RefRFC-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

SeverityIDTitleRFC Section
CRITICALSEC-099-C1Header spoofing — no strippingRFC-063 §Security Gaps #1
CRITICALSEC-099-C2Go backend trusts advisory headersRFC-063 §3
CRITICALSEC-099-C3Unauthenticated gets full accessRFC-063 §Unauthenticated Mode
CRITICALSEC-099-C4SCIM bearer token timing attackRFC-065 §Unauthorized Grants
CRITICALSEC-099-C5No SAML signature validationRFC-064 §Provider Rules
HIGHSEC-099-H1Unbounded SAML replay cacheRFC-064 §Provider Rules
HIGHSEC-099-H2No input validation on identifiersRFC-064 §Canonical Subjects, RFC-065 §Resource Model
HIGHSEC-099-H3SCIM store TOCTOU racesRFC-065 §Idempotency
HIGHSEC-099-H4Unbounded ScimUser extra fieldsRFC-065 §Resource Model
HIGHSEC-099-H5Provider ID not whitelistedRFC-065 §SCIM Writes
HIGHSEC-099-H6First-key-only JWKS fetchRFC-064 §OIDC Providers
HIGHSEC-099-H7Backend token audience disabledRFC-063 §Backend Token Claims
MEDIUMSEC-099-M1Issuer slug collisionsRFC-064 §Canonical Subjects
MEDIUMSEC-099-M2Optional recipient validationRFC-064 §Provider Rules
MEDIUMSEC-099-M3Hard delete, not lifecycleRFC-065 §Deprovisioning
MEDIUMSEC-099-M4Reconciliation reports onlyRFC-065 §Deprovisioning
MEDIUMSEC-099-M5SCIM Replace is no-opRFC-065 §Sync Semantics
MEDIUMSEC-099-M6No rate limitingRFC-065 §Blast Radius
MEDIUMSEC-099-M7Linear search timing oracleRFC-063 §Namespace Security
MEDIUMSEC-099-M8issued_at falls back to epoch 0
LOWSEC-099-L1Bearer token plaintext in memory
LOWSEC-099-L2No namespace format validationRFC-063 §Proxy Backplane
LOWSEC-099-L3Raw key bytes from backplane
LOWSEC-099-L4Double write lock acquisition
LOWSEC-099-L5SAML issuer not validatedRFC-064 §Provider Rules
LOWSEC-099-L6Test policies in production path
INFOSEC-099-I1No SAML-SCIM identity bindingRFC-064 §Canonical Subjects, RFC-065 §Resource Model
INFOSEC-099-I2No audit loggingRFC-063 §6, RFC-064 §Provider Drift
INFOSEC-099-I3Safe defaults (positive)
INFOSEC-099-I4Serde flatten schema override
INFOSEC-099-I5No typed SAML AuthContext constructor
INFOSEC-099-I6Backplane bootstrapping raceRFC-063 §Open Questions

By OWASP Category

OWASPFindings
A01: Broken Access ControlC1, C2, C3, H5
A02: Cryptographic FailuresC5, H6, H7
A03: InjectionH2
A04: Insecure DesignH3
A05: Security MisconfigurationH1
A07: Identification and Auth FailuresC2, C4, C5
A08: Software and Data IntegrityH4

By RFC

RFCCriticalHighMedium+Low+InfoTotal
RFC-063 (Proxy Auth Contract)3159
RFC-064 (SAML Federation)1337
RFC-065 (SCIM Provisioning)1359
Implementation (no RFC gap)0033

Remediation Priority

Phase 1: Immediate (Blocks any production deployment)

  1. SEC-099-C1: Wire header stripping (helper exists, ~5 lines)
  2. SEC-099-C4: Constant-time bearer token comparison (~3 lines)
  3. SEC-099-C3: Remove return true in Go HasPermission (~2 lines)

Phase 2: Required Before Federation Ships

  1. SEC-099-C5: Add SignatureVerifier trait to SAML validator
  2. SEC-099-C2: Implement ValidateBackendToken in Go
  3. SEC-099-H2: Add identifier validation functions
  4. SEC-099-M1: Fix slugify_issuer to use full hostname

Phase 3: Hardening

  1. SEC-099-H1: Bound SAML replay cache, add LRU eviction
  2. SEC-099-H3: Refactor SCIM store to single-lock operations
  3. SEC-099-H4: Cap extra field size
  4. SEC-099-M2: Make SAML recipient required
  5. SEC-099-M3/M4: Implement lifecycle transitions and active reconciliation
  6. SEC-099-H6: Iterate all JWKS keys
  7. SEC-099-I1/I2: Implement SAML-SCIM binding and audit logging

Revision History

  • 2026-04-15: Initial security review — 32 findings (5 Critical, 7 High, 8 Medium, 6 Low, 6 Info)