blob: 6737302a3061c22bce825b4aa3ab028f0b4dbd75 [file] [view]
# Apache Directory SCIMple — Threat Model
## §1 Header
- **Project:** Apache Directory SCIMple — a Jakarta EE implementation of SCIM 2.0 (RFC 7642 / RFC 7643 / RFC 7644).
- **Applies to:** the SCIMple revision this file is committed with; each release's git tag binds its own copy (see *Version binding*).
- **Date:** 2026-05-30.
- **Authors of this model:** the Apache Directory PMC.
- **Status:** **Draft, prepared by the Apache Directory PMC (2026-05-30).** Not yet formally reviewed or ratified;
submitted for PMC review and endorsement via lazy consensus on `dev@directory.apache.org`.
### Version binding
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.
### Reporting cross-reference
- Findings that violate a property **claimed in §8** should be reported **privately**
per [`SECURITY.md`](SECURITY.md) (the ASF security process) — never via a public GitHub issue.
- Findings that fall under **§3 (out of scope)** or **§9 (disclaimed properties)** will be closed citing this document;
see the disposition table in §13.
### Evidence tags
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.
### What this project is
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`.
---
## §2 Scope and intended use
**Primary intended use cases:**
- In-process embedding of SCIM 2.0 **server** endpoints into a host application that supplies its own `Repository`
persistence and its own authentication layer.
- In-process embedding of a SCIM 2.0 **client** to provision against a remote SCIM service.
- Use of the **schema / spec** libraries (resource models, filter parser, PATCH path parser) independently of either.
**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). |
### Component-family table
| 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 |
---
## §3 Out of scope (explicit non-goals)
**Use cases the project does not aim to support:**
- A complete, secure identity server out of the box. SCIMple supplies the SCIM protocol layer only; **authentication,
authorization, transport security, rate limiting, and persistence are the integrator's responsibility.**
- A query/persistence engine. SCIMple parses SCIM filters into structured
`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:**
- Network-level attacks (TLS interception, traffic analysis) — the host terminates TLS.
- Authentication/authorization bypass — there is no SCIMple-owned authN/authZ to bypass (§8/§9); this is the
integrator's layer.
- **Abuse of legitimately-granted SCIM authority.** SCIM is a high-privilege surface — a client authorized to
create/modify/delete users and groups (and set passwords) already wields broad power over identities. A caller using
*granted* authority to harmful effect (creating a rogue admin, mass-deleting, resetting a password) is performing an
authorized operation; SCIMple executing it faithfully is correct behavior, not a vulnerability. Constraining what each
authenticated client may do — least privilege, per-operation authorization — is the integrator's responsibility
(§9, §10). This is distinct from **subverting SCIMple's own machinery** (crash, secret leak, injection past the parsed
boundary), which SCIMple *does* defend regardless of caller privilege (§8).
- Attacks requiring control of the embedding process, the `Repository`, the registered schemas, or the JAX-RS `Client` —
that is trusted code (§7).
- A malicious or compromised **remote SCIM server** that `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.
---
## §4 Trust boundaries and data flow
**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):**
1. Untrusted bytes arrive at a JAX-RS endpoint (`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.**
2. The endpoint resolves the target `Repository` from `RepositoryRegistry` and invokes it with **already-parsed,
structured objects** — never the raw request strings.
3. The integrator's `Repository` (trusted code) performs persistence and returns resources.
4. On the way out, `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.
### Reachability preconditions per component
- **scim-spec-schema (filter/PATCH/JSON parsing):** in-model only if reachable from **attacker-controlled input** — a
`filter` query string, a PATCH `path`, or a request body.
- **scim-core (Repository SPI):** in-model only if the issue is in **SCIMple's marshaling/dispatch**, not in the
integrator's `Repository`.
- **scim-server (endpoints):** in-model only if reachable by an **unauthenticated or authenticated-but-untrusted HTTP
client** through the standard endpoints.
- **scim-client:** outside the adversary model — the remote server is trusted (§7) and the `Client` is the integrator's.
Its obligations are integrator responsibilities (§9/§10), not adversarial-input handling.
---
## §5 Assumptions about the environment
- **Runtime:** a current, supported Java LTS — the exact floor is enforced by the build (17+ at time of writing).
Memory-safety assumptions rest on running on a supported JVM.
- **Concurrency:** `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)*.
- **Memory model:** standard JVM; no native code in the published libraries.
### What the project does *not* do to its host (negative side-effect inventory)
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:
- do **not** open listening sockets (the host container does);
- do **not** spawn child processes;
- do **not** read environment variables or system properties for behavior;
- do **not** read or write files;
- do **not** install signal/shutdown handlers;
- **do** emit logging via SLF4J, and **do log resource objects and bulk dependency graphs at DEBUG level** in
`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).
- hold a process-wide `static final` Jackson `ObjectMapper`; otherwise do not mutate global/process state.
---
## §5a Build-time and configuration variants
**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.
---
## §6 Assumptions about inputs
### Server endpoints (`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) |
### Client (`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).
---
## §7 Adversary model
**Assumed attacker:**
- **Untrusted SCIM client** — can reach the server endpoints the integrator exposes and send arbitrary bodies, filter
strings, PATCH paths, bulk operations, headers, and query params. May be unauthenticated (SCIMple enforces no authN
itself) unless the integrator's layer blocks them first.
**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:**
- Anyone with control of the embedding process or the integrator's `Repository`/
`Client`/schema registration — trusted code; they have already won.
- The **remote SCIM server** that `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).
- Local attackers reading DEBUG logs — a host operational concern (§9/§10).
This project is **not** a distributed/consensus system, so Byzantine-peer and honest-fraction concepts do not apply.
---
## §8 Security properties the project provides
> Each entry: **property + conditions / violation symptom / severity / evidence.**
> Severity is **SECURITY-CRITICAL** (warrants coordinated disclosure per `SECURITY.md`)
> or **CORRECTNESS-ONLY** (ordinary bug).
1. **All `Returned.NEVER` attributes (the only core one being `password`) are stripped from responses, and `password` is
excluded from resource `toString()`.**
- *Conditions:* response flows through `AttributeUtil.setAttributesForDisplay`;
`password` is `returned = NEVER`.
- *Violation symptom:* a `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`.
2. **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.
3. **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).
4. **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.
5. **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.
6. **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).
7. **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).
- *Violation symptom:* small/cheap request → `StackOverflowError`, hang, or exponential CPU/allocation. *Severity:*
**SECURITY-CRITICAL** (DoS).
- *Provided:* `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.
8. **Enforcement of advertised request limits.** SCIMple enforces the configured
`bulkMaxOperations`, `bulkMaxPayloadSize`, and `filterMaxResults` in the request path.
- *Violation symptom:* a request exceeding a configured limit is processed anyway. *Severity:* **SECURITY-CRITICAL**
(DoS/fan-out) for the bulk bounds; **CORRECTNESS-ONLY** for `filterMaxResults`.
- *Provided:* a `/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`.
---
## §9 Security properties the project does *not* provide
The most valuable section for an integrator. These are disclaimed **by design** unless a code citation notes otherwise:
- **No authentication.** SCIMple performs no authN of SCIM clients. Endpoints other than
`/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`.)*
- **No authorization.** No role/permission checks anywhere.
- **No transport security.** `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.)*
- **No resistance to proportional resource cost.** SCIMple bounds *catastrophic* input (§8.7) but makes **no guarantee
about resources consumed in proportion to a large but well-formed input** (a huge body, a long flat filter, a large
bulk batch within the intended limits). Bounding total input size/rate is the integrator's / container's job.
- **No defense against a malicious `Repository`, schema set, or `Client`.** Trusted code (§7).
- **No password hashing or at-rest protection.** SCIMple passes the cleartext `password`
to `Repository.create/update`; hashing, storage, and never-logging are the integrator's responsibility. *(
documented.)*
- **No protection against operator-enabled verbose logging.** With DEBUG enabled,
`scim-server` logs non-secret resource contents (§5/§10). `password` is excluded (§8.1), but other attributes are not.
### False-friend properties (look like security, 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.)
### Well-known attack classes left to the caller
- **Oversized / high-rate input** (bodies, long filters, large bulk batches within limits) → proportional CPU/heap:
bound size and rate upstream.
- **Filter → backend injection** (SQL/LDAP): SCIMple hands you a parsed
`FilterExpression`; parameterize any backend query you build from it.
- **SSRF via `scim-client` `baseUrl`:** validate the target before pointing the client at it.
- **Mass-assignment / over-posting** on SCIM resources: validate which attributes a given caller may set.
- **ReDoS** on the `@Urn` validator regex against malformed URNs *(low-likelihood; see §11a; note §8.7 would classify a
genuinely super-linear case as a bug)*.
---
## §10 Downstream responsibilities
For SCIMple, "downstream" = the **integrator/operator**. To make §5–§7 hold, they MUST:
1. **Authenticate and authorize** every SCIM request before it reaches SCIMple endpoints. SCIM is a high-privilege
surface (it manages users, groups, and passwords), so apply **least privilege**: scope what each SCIM client may do
rather than granting blanket create/modify/delete. A compromised or over-privileged client is "game over" at the
identity layer no matter how robust SCIMple's parsing is — this authorization boundary is the primary control.
2. **Terminate and verify TLS** for inbound serving and the outbound `scim-client`
`Client` (certificate + hostname verification, sane timeouts).
3. **Bound untrusted input** at the edge: request body size, bulk batch size, and request rate. (SCIMple bounds
*catastrophic* parser cases per §8.7 but not proportional cost.)
4. **Implement the `Repository` securely:** hash passwords, never log secrets, parameterize any backend query built from
a `FilterExpression`, and enforce per-caller authorization on every operation.
5. **Do not enable DEBUG logging in production**, or scrub it — `scim-server` logs non-secret resource contents at DEBUG
(§5). (`password` is excluded.)
6. **If you point `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).
---
## §11 Known misuse patterns
- **Exposing SCIMple endpoints directly to the internet with no auth in front.** The endpoints ship without authN/authZ;
unguarded exposure yields unauthenticated CRUD over users/groups. *Instead:* gate every route behind the host's auth
layer.
- **Assuming `/ServiceProviderConfig` `supports*` flags enforce behavior.** *Instead:*
enforce capability decisions in the integrator/container; the flags only advertise.
- **Relying on SCIMple to bound all input cost.** Forwarding unbounded bodies/batches. *Instead:* cap size and rate at
the edge (SCIMple covers catastrophic parser cases only, §8.7).
- **Building backend queries from `FilterExpression` via string concatenation.**
Injection. *Instead:* parameterize / use `BaseFilterExpressionMapper` patterns.
- **Treating response `password` redaction as secret management.** *Instead:* hash and protect the secret in the
`Repository`.
- **Logging at DEBUG in production.** Leaks non-secret resource contents. *Instead:* keep production at INFO+ or scrub.
---
## §11a Known non-findings (recurring false positives)
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).
- **"XXE in JAXB."** Safe: no untrusted XML is parsed — no `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):**
- **Wrong CPE/GAV matches:** SCIMple JARs mis-identified as Apache HTTP Server; `junit-platform-engine` as the
"fan_platform" Python project; `commons-codec` / `commons-logging` / `jcl-over-slf4j` mis-linked to `commons_net`.
- **Weld `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.
- **SnakeYAML `CVE-2022-1471`** — transitive dependency; the unsafe `Constructor` path that triggers it is not used.
---
## §12 Conditions that would change this model
Revise this document when any of the following occurs:
- A new public endpoint, accepted input format, or protocol message is added.
- SCIMple begins parsing untrusted XML, or enables Jackson default typing (would void §8.3/§8.5), or overrides Jackson's
`StreamReadConstraints`.
- A core attribute beyond `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).
- A shipped-but-unsupported component (an example, the LDAP reference project) is promoted into a published artifact.
- **Evidence the model is incomplete:** a vulnerability report that cannot be cleanly routed to a §13 disposition is
itself a trigger (`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).
---
## §13 Triage dispositions
| 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 |
---
## §14 Machine-readable companion
A sidecar [`threat-model.yaml`](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.