dev@directory.apache.org.This threat model is versioned alongside the project. A vulnerability report filed against project version N is triaged against this model as it stood at N, not as it stands at HEAD. Because the file lives in the repository, the git tag for a release automatically binds that release's copy of the model — no separate snapshot process is needed; the THREAT_MODEL.md at a release's tag is the model for that release. The publication venue is the repository (canonical); SECURITY.md links to it.
SECURITY.md (the ASF security process) — never via a public GitHub issue.Claims marked (documented: …) cite their source — code, a committed test, the README, or an RFC. All other statements are assertions by the PMC for this draft.
Apache Directory SCIMple is a set of embeddable Java libraries that let an application speak SCIM 2.0 — the standard REST protocol for provisioning users and groups across identity systems. It is not a turnkey identity server. The integrator brings their own persistence by implementing a Repository interface and their own deployment container (Spring Boot, Quarkus, Jersey, a servlet container, …); SCIMple supplies the SCIM wire protocol, the schema/extension model, the filter and PATCH expression parsers, the REST endpoint implementations, and an outbound SCIM client. The libraries are published to Maven Central under org.apache.directory.scimple.
Primary intended use cases:
Repository persistence and its own authentication layer.Deployment contexts: an embedded library inside a JVM application — delivered through Spring Boot, Quarkus, Jersey, or a Jakarta-EE servlet container. SCIMple does not run as a standalone daemon; the host owns the process, the listening socket, TLS termination, and the authentication boundary.
Caller roles. The server side is exposed over HTTP, so “caller” is not a single trust level:
| Role | Trust | Description |
|---|---|---|
SCIM client — an external caller of your scim-server (not the scim-client library) | untrusted | Sends HTTP requests with attacker-controllable bodies, filter strings, PATCH paths, headers, and query params. |
| Integrator / operator | trusted | Writes the Repository implementation, wires the deployment, configures auth/TLS, registers schemas. Authoring this code = already inside the trust boundary. |
Remote SCIM peer (the server scim-client calls) | trusted | The server an integrator points scim-client at — chosen by the integrator, who holds credentials for it and trusts it. A malicious or compromised remote server is out of scope (§3, §7). |
| Family | Representative entry point | Touches outside the process? | In model? |
|---|---|---|---|
| scim-spec-schema (parsing) | SCIM JSON models, ANTLR filter grammar, PATCH path parser, URN/@Urn validator | No (pure parsing/validation) | In |
| scim-spec-protocol | REST protocol models (SearchRequest, BulkRequest, ErrorResponse), JAX-RS interfaces | No | In |
| scim-core | Repository<T> SPI, SchemaRegistry, RepositoryRegistry, Jackson ObjectMapper/deserializer | No | In (the SPI boundary is in model; integrator impls are out) |
| scim-server | JAX-RS endpoint impls: /Users, /Groups, /Bulk, /Me, /Schemas, /ResourceTypes, /ServiceProviderConfig, .search | Inbound HTTP (via the host container) | In |
| scim-client | BaseScimClient and friends — outbound SCIM REST calls over a caller-supplied JAX-RS Client | Outbound HTTP (via caller-supplied client) | In (integrator obligations only; the remote peer is trusted — §7) |
| support/spring-boot | scim-spring-boot-starter auto-configuration | No (wiring only) | In (wiring) |
| scim-server-examples/, reference-projects/scim-server-ldap | demo apps (in-memory, Jersey, Quarkus, Spring Boot, LDAP) | Yes (open sockets, persist data) | Out — see §3 |
| scim-test, scim-tools, scim-compliance-tests, scim-coverage | test/CI tooling | varies (test only) | Out — see §3 |
Use cases the project does not aim to support:
FilterExpression trees and hands them to the integrator's Repository; it does not translate them to SQL/LDAP/etc. (BaseFilterExpressionMapper is a helper for integrators who choose to, not a SCIMple-owned query builder).Threats not defended against at this layer:
Repository, the registered schemas, or the JAX-RS Client — that is trusted code (§7).scim-client connects to. The integrator chooses, trusts, and authenticates to that server; scim-client is a library under the integrator's control, not an adversary-facing surface. SCIMple does not harden scim-client against hostile responses from the server it calls.Code that ships in the repo but is out of this model:
scim-server-examples/** and reference-projects/scim-server-ldap — example/demo applications, not published to Maven Central (enforced: the scim-server-examples parent pom sets maven.deploy.skip / skipNexusStagingDeployMojo). They open sockets and store data (the InMemoryUserService examples hold passwords in plaintext) and are not production-hardened. Findings here are OUT-OF-MODEL: unsupported-component (§13). The example READMEs carry a demo-only warning.scim-test, scim-tools, scim-compliance-tests, scim-coverage — test/CI tooling.Where the boundary sits. The trust boundary is the HTTP/API surface of scim-server — untrusted bytes cross into the process there. scim-client calls out to a server the integrator chose and trusts (§7), so its outbound side is not an adversarial trust boundary; the integrator owns its transport configuration (§9/§10).
Data flow (inbound request):
scim-server). The body is deserialized by Jackson into strongly-typed SCIM resource objects; filter/PATCH path strings are parsed by the ANTLR grammar into FilterExpression / PatchOperationPath trees. This is the parse step — the richest attack surface.Repository from RepositoryRegistry and invokes it with already-parsed, structured objects — never the raw request strings.Repository (trusted code) performs persistence and returns resources.AttributeUtil strips returned = NEVER attributes (e.g. password) and applies attribute selection before serialization.The key transition: untrusted text → structured object happens entirely inside SCIMple before any integrator code runs. This is what makes the Repository SPI a clean boundary.
filter query string, a PATCH path, or a request body.Repository.Client is the integrator's. Its obligations are integrator responsibilities (§9/§10), not adversarial-input handling.SchemaRegistry and RepositoryRegistry are populated at startup by trusted integrator code and read concurrently thereafter; their backing maps are not synchronized for concurrent registration (only registerRepository is synchronized). Assumption: registration completes before request handling begins. The shared Jackson ObjectMapper is a static final singleton, thread-safe once configured (documented: Jackson).Verified by a source sweep across scim-spec, scim-core, scim-server, scim-client, and support/spring-boot main sources (verified 2026-05-30). The published libraries:
scim-server (BulkResourceImpl, BaseResourceTypeResourceImpl). With DEBUG enabled, non-secret SCIM attribute data can appear in logs — an operator responsibility (§10). The password attribute is not exposed this way (§8.1).static final Jackson ObjectMapper; otherwise do not mutate global/process state.Runtime configuration knobs (ServerConfiguration) (documented: source). These values are surfaced in the /ServiceProviderConfig response, and the resource-limit knobs are enforced in the request path (§8.8):
| Knob | Default | Intended contract | Enforced today? |
|---|---|---|---|
bulkMaxOperations | 100 | enforced bound on bulk fan-out | Yes (§8.8) |
bulkMaxPayloadSize | 1024 | enforced bound | Yes (§8.8) |
filterMaxResults | 100 | enforced result bound | Yes (§8.8) |
supportsBulk / supportsFilter / supportsPatch / supportsSort / supportsETag / supportsChangePassword | varies | capability advertisement | advisory only |
authenticationSchemas | empty | capability advertisement | advisory only |
The capability supports* flags remain advertisements — they describe what the deployment offers, not access controls.
Build-time security tooling (documented: root pom.xml, .github/) — changes what the build guarantees, relevant to §11a/§12: OWASP Dependency-Check (build fails on any CVE), SpotBugs + find-sec-bugs (max effort), PMD, CodeQL, Dependabot, and a CycloneDX SBOM on release. (Exact tool versions live in the build, not here.) No build flag changes a runtime security property in the published libraries.
scim-server)| Endpoint / message | Parameter | Attacker-controllable? | Caller (integrator) must enforce |
|---|---|---|---|
GET /Users, /Groups | filter (string) | yes | authN before exposure |
GET /Users, /Groups | startIndex, count | yes | — (result size capped at filterMaxResults, §8.8) |
GET … | attributes, excludedAttributes | yes | — (mutually-exclusive check exists) |
POST/PUT /Users, /Groups, /Me | JSON body | yes | body-size limits (container), schema trust |
PATCH /Users/{id}, /Me | PatchRequest body incl. path | yes | — (parsed; nesting depth capped, §8.7) |
POST /Bulk | BulkRequest (operation array, failOnErrors) | yes | — (operation count & payload size enforced, §8.8) |
| any mutating | If-Match header | yes | — (parsed by EtagParser) |
GET /Me, PUT/PATCH/DELETE /Me | caller principal | via container | authentication (endpoint requires a SecurityContext principal) |
GET /Schemas, /ResourceTypes | filter | yes | — (SCIMple returns 403 if present, per RFC) |
scim-client)| Message | Parameter | Attacker-controllable? | Caller must enforce |
|---|---|---|---|
| outbound call | baseUrl | no — caller-supplied | SSRF-safe URL |
| outbound call | the JAX-RS Client (TLS, creds, timeouts, redirects) | no — caller-supplied | TLS verification, timeouts, credential handling |
| inbound response | remote SCIM peer's JSON body | no — the integrator trusts the server it calls | (out of model — §3, §7) |
Size / shape / rate: SCIMple relies on Jackson's built-in StreamReadConstraints (max nesting depth, string/number length bounds) for JSON, and neither tightens nor loosens them — these defaults satisfy the §8.7 “catastrophic” line for JSON depth/size. The ANTLR filter/PATCH grammar is recursive; SCIMple caps nesting depth at FilterParsers.MAX_NESTING_DEPTH = 40 (§8.7). There is no input-length cap — proportional cost is the integrator‘s/container’s job (§9).
Assumed attacker:
Capabilities they have: craft malformed/oversized/deeply-nested SCIM payloads; craft pathological filter/PATCH expressions; submit large bulk batches. Capabilities they do not have: read/modify the embedding process's memory; alter the Repository, the registered schemas, or the Client configuration; observe local logs.
Attacker goals in scope: crash/hang the host via a small crafted input or super-linear cost (§8.7); read data they shouldn‘t (e.g. password leakage); escape the parsed-object boundary or abuse deserialization; trigger malformed-input failures. These goals are about **subverting SCIMple’s own machinery**, and they hold regardless of how privileged the authenticated caller is. They are not the consequences of operations the caller was authorized to perform: SCIM is a high-privilege surface, and the blast radius of granted authority is governed by the integrator's authorization (§3, §10), not by SCIMple.
Actors explicitly NOT in the model:
Repository/ Client/schema registration — trusted code; they have already won.scim-client connects to — the integrator selects it, holds credentials for it, and trusts it; a malicious/compromised remote server is out of scope (§3).This project is not a distributed/consensus system, so Byzantine-peer and honest-fraction concepts do not apply.
Each entry: property + conditions / violation symptom / severity / evidence. Severity is SECURITY-CRITICAL (warrants coordinated disclosure per
SECURITY.md) or CORRECTNESS-ONLY (ordinary bug).
Returned.NEVER attributes (the only core one being password) are stripped from responses, and password is excluded from resource toString().AttributeUtil.setAttributesForDisplay; password is returned = NEVER.password (or other NEVER attribute) appears in a SCIM response body or in logs via toString(). Severity: SECURITY-CRITICAL (sensitive-data disclosure). Evidence: (documented) — ScimUser.password is returned = NEVER; AttributeUtil strips NEVER attributes; AttributeUtilTest asserts getPassword() is null after display processing; ScimUser.toString() omits password. Regression-tested: AttributeUtilTest guards NEVER attributes out of responses (even when explicitly requested), and ScimUserTest guards every Returned.NEVER attribute out of ScimUser.toString() (schema-driven, so future NEVER fields are covered). Merged in develop.Untrusted filter / PATCH strings are parsed into structured objects before reaching the integrator's Repository. Severity: CORRECTNESS-ONLY for SCIMple (any backend injection happens in integrator code; §9). Evidence: (documented) — Repository.find(Filter, …) / patch(id, List<PatchOperation>, …) take parsed types.
No unsafe Jackson polymorphic deserialization. Violation symptom: gadget-chain deserialization (RCE). Severity: SECURITY-CRITICAL if violated. Evidence: (documented) — no enableDefaultTyping/activateDefaultTyping; no @JsonTypeInfo/@JsonSubTypes on SCIM models (verified absent).
Schema/extension registration loads no classes from untrusted input. Severity: SECURITY-CRITICAL if violated. Evidence: (documented) — registration is an explicit integrator API; no Class.forName from request data.
No untrusted XML is parsed (no XXE surface in the published libraries). Severity: SECURITY-CRITICAL if violated. Evidence: (documented) — no XMLInputFactory/SAXParser/DocumentBuilderFactory/Unmarshaller in scim-spec/scim-core (verified); JAXB API is provided, annotation-only. See §11a.
Filter / PATCH round-trip fidelity. Severity: CORRECTNESS-ONLY. Evidence: (documented) — FilterBuilderTest, PatchOperationPathTest. Caveat: JSON string values are extracted by substring without unescaping (ExpressionBuildingListener.parseJsonType), so escaped sequences may not round-trip faithfully (correctness-only).
Catastrophic-input resistance (resource bound). SCIMple commits to a categorical line: a small input that causes a crash (e.g. StackOverflowError from a deeply nested filter/PATCH) or super-linear / exponential cost in input size is a bug SCIMple will fix; cost merely proportional to a large input is not (the integrator's job — §9).
StackOverflowError, hang, or exponential CPU/allocation. Severity: SECURITY-CRITICAL (DoS).FilterParsers.MAX_NESTING_DEPTH (= 40) caps nesting by counting entries into the recursive filterExpression / attributeExpression grammar rules during parsing (a DepthCountingListener aborts before the recursive-descent parser can overflow the stack), for both the filter parser and the PATCH-path parser. Over-limit input is rejected as FilterParseException with a fixed message that does not echo the input. JSON depth/size is bounded by Jackson defaults (§6). Evidence: (documented) — FilterParsers (MAX_NESTING_DEPTH = 40, DepthCountingListener), merged in develop. There is no input-length cap; proportional cost is disclaimed in §9.bulkMaxOperations, bulkMaxPayloadSize, and filterMaxResults in the request path.filterMaxResults./Bulk request exceeding bulkMaxOperations is rejected with HTTP 413 (scimType tooMany, per RFC 7644 §3.7.4) before any operation runs (BulkResourceImpl); bulkMaxPayloadSize is enforced by BulkPayloadSizeFilter; query results are capped at filterMaxResults (BaseResourceTypeResourceImpl). Evidence: (documented) — merged in develop.The most valuable section for an integrator. These are disclaimed by design unless a code citation notes otherwise:
/Me are reachable by anyone who can reach the route; /Me merely requires that a SecurityContext principal already exists (the container/integrator must supply it). Securing the endpoints is the integrator's job. (documented: no ContainerRequestFilter/@RolesAllowed; SecurityContext used only in SelfResourceImpl.)scim-client does not configure TLS; it uses the caller-supplied JAX-RS Client. Certificate/hostname verification, timeouts, redirect policy, and credentials are entirely the caller's responsibility. (documented: no SSLContext/TrustManager/HostnameVerifier/timeout code.)Repository, schema set, or Client. Trusted code (§7).password to Repository.create/update; hashing, storage, and never-logging are the integrator's responsibility. ( documented.)scim-server logs non-secret resource contents (§5/§10). password is excluded (§8.1), but other attributes are not.returned = NEVER password redaction. It removes password from responses and from toString() (a real property, §8.1). It does not mean SCIMple protects the password elsewhere: the value is held in memory during request processing and passed in cleartext to the Repository. It is response/secret-in-log redaction, not secret management./ServiceProviderConfig supports* capability flags. supportsBulk=false, supportsFilter=false, etc. look like enforcement switches; they are advertisements only — the endpoints behave identically regardless. (The numeric limit knobs are different — bulkMaxOperations / bulkMaxPayloadSize / filterMaxResults are enforced; see §5a/§8.8.)FilterExpression; parameterize any backend query you build from it.scim-client baseUrl: validate the target before pointing the client at it.@Urn validator regex against malformed URNs (low-likelihood; see §11a; note §8.7 would classify a genuinely super-linear case as a bug).For SCIMple, “downstream” = the integrator/operator. To make §5–§7 hold, they MUST:
scim-client Client (certificate + hostname verification, sane timeouts).Repository securely: hash passwords, never log secrets, parameterize any backend query built from a FilterExpression, and enforce per-caller authorization on every operation.scim-server logs non-secret resource contents at DEBUG (§5). (password is excluded.)scim-client at a server you do not control or fully trust, treat its responses as untrusted input — the default model assumes a trusted, integrator-chosen peer (§7)./ServiceProviderConfig supports* flags enforce behavior. Instead: enforce capability decisions in the integrator/container; the flags only advertise.FilterExpression via string concatenation. Injection. Instead: parameterize / use BaseFilterExpressionMapper patterns.password redaction as secret management. Instead: hash and protect the secret in the Repository.Feed this back to tooling as a suppression list.
Reasoned non-findings (not in a suppression file, discharged by a §8 property):
jackson-databind “unsafe deserialization.” Safe: no default typing and no @JsonTypeInfo/@JsonSubTypes in the published sources (verified); SCIM payloads bind to a fixed model (§8.3).XMLInputFactory/DocumentBuilderFactory/SAXParser/ Unmarshaller in scim-spec/scim-core (verified); JAXB API is annotation-only, provided scope (§8.5).Suppressed in src/spotbugs/excludes.xml:
EI_EXPOSE_REP / EI_EXPOSE_REP2 (object exposes its internal representation) — suppressed for the spec.filter.* and spec.phonenumber.* model packages. Mutable-field exposure on value/model objects, not an attacker-input issue.REDOS on UrnValidator — the URN-validation regex is suppressed as a false positive (the maintainers judged it non-backtracking). Note: a URN can arrive in request data, so per §8.7 a genuinely super-linear case would be a VALID bug — re-evaluate if that regex changes.UNENCRYPTED_SERVER_SOCKET — only in EmbeddedServerExtension (the scim-compliance-tests integration-test harness); out of scope per §3.RCN_REDUNDANT_NULLCHECK_… (suppressed globally) and the PhoneNumber builder findings (UWF_FIELD_NOT_INITIALIZED…, NP_NULL_ON_SOME_PATH…), ObjectMapperFactory MS_EXPOSE_REP, and BaseScimClient CT_CONSTRUCTOR_THROW — code-quality false positives (largely Lombok-generated code), not security-relevant.Suppressed in src/owasp/suppression.xml (dependency-scan noise):
junit-platform-engine as the “fan_platform” Python project; commons-codec / commons-logging / jcl-over-slf4j mis-linked to commons_net.probe.js (old jQuery/Bootstrap) CVEs. weld-se-core bundles probe.js; it is test-scoped in the published libraries (scim-client, scim-server) and a runtime dependency only of the Jersey demo examples. The JS asset is never in a published artifact's runtime surface and SCIMple never serves it.CVE-2022-1471 — transitive dependency; the unsafe Constructor path that triggers it is not used.Revise this document when any of the following occurs:
StreamReadConstraints.password is marked Returned.NEVER, or ScimUser.toString() is regenerated (e.g. switched to Lombok @Data) — re-verify §8.1.scim-client gains its own transport/credential handling, or SCIMple adds authN/authZ (would move items from §9 to §8).MODEL-GAP) — add the missing property to §8 or §9 rather than making an ad-hoc call.Ownership/cadence: the Apache Directory PMC owns this document; it is re-reviewed at each release and whenever a trigger above fires. Each release‘s git tag binds that release’s copy (§1).
| Disposition | Meaning | Licensed by |
|---|---|---|
VALID | Violates a property the project claims, via an in-scope adversary and input. | §8, §6, §7 |
VALID-HARDENING | No §8 property is violated, but the API makes a §11 misuse easy enough that the project elects to harden it. Reported privately; fixed at maintainer discretion; typically no CVE. | §11 |
OUT-OF-MODEL: trusted-input | Requires attacker control of a parameter the model marks trusted (e.g. baseUrl, the Repository, registered schemas). | §6 |
OUT-OF-MODEL: adversary-not-in-scope | Requires a capability the model excludes (e.g. control of the embedding process). | §7 |
OUT-OF-MODEL: unsupported-component | Lands in scim-server-examples/**, reference-projects/**, or test/CI tooling. | §3 |
OUT-OF-MODEL: non-default-build | Only manifests under a discouraged or non-default §5a configuration. | §5a |
BY-DESIGN: property-disclaimed | Concerns a property the project explicitly does not provide (authN/authZ, transport, proportional resource cost, password hashing). | §9 |
KNOWN-NON-FINDING | Matches a documented recurring false positive. | §11a |
MODEL-GAP | Cannot be cleanly routed to any of the above → revise the model. | triggers §12 |
A sidecar threat-model.yaml accompanies this prose for automated/AI triage, carrying only the triage-relevant facts: entry points → per-parameter trust (§6), component families → in/out of scope (§2/§3), config knobs → security-relevant?/default?/enforced? (§5a), claimed properties → severity + violation symptom (§8), disclaimed properties + false friends (§9), known non-findings (§11a), and the disposition labels (§13).
The prose remains canonical; the YAML is a derived index. It pins no version — the release git tag binds both files (§1). Regenerate it whenever this prose changes.