ndovuscim

SCIM filter playgroundno filter run yetRuns in your browser - nothing leaves this page.

Parsing is recursive descent with a depth cap of 20; nesting beyond the cap is rejected instead of risking unbounded recursion. The cap is the tool teaching the lesson it documents: the Keycloak and linagora/scim2 parsers both fixed CVEs of this shape (GitHub issues #50472, June 2026, and #471, October 2026).

RFC 7644 · SECTION 3.4.2.2 FILTERING

The filter playground

Evaluate RFC 7644 Section 3.4.2.2 filters against sample resources, with the parse tree, evaluator semantics, and the open grammar errata on display.

Runs in your browser - nothing leaves this page.RFC 7644 Section 3.4.2.2

The playground parses a filter into its abstract syntax tree, evaluates it against the corrected RFC 7643 example resources, and renders the ListResponse envelope a compliant server would return. The provider toggles model the second dimension real integrations fail on: the operator subsets and matching-attribute allowlists that providers document in place of the full grammar. This page is a complement to conformance tools, not another validator: conformance suites exercise a live provider over the wire, while this playground lets one filter be read apart from the network.

The history

The failure this playground teaches

SCIM filtering descends from the LDAP search filter line (RFC 2254, carried into RFC 4515), through the SCIM 1.0 and 1.1 drafts, into RFC 7644 Section 3.4.2.2. The grammar kept the shape; the deployment reality did not. Providers document subsets: Entra sends eq and and on configured matching attributes, GitHub EMU accepts one eq at a time, Zoom documents exactly two filters, and Okta requires userName eq for user matching. A filter that is valid per the RFC can still be unsupported on the provider that must process it, and the provider answers 400.

The reverse failure also exists: parsers that reject filters the grammar accepts. The GitHub survey for this site (2026-11-01) records SimpleIdServer #419 and pingidentity/scim2 #214 as valid filters rejected, and the grammar itself carries three open disputes (errata 7319, 7322, and 4670 below), so two correct parsers can disagree about the same input.

The third failure is operational: parsing is recursive descent, and unbounded recursion on a hostile filter is a known parser-denial-of-service class. keycloak/keycloak #50472 (June 2026) and linagora/scim2 #471 (October 2026) both fixed CVEs of this shape. The playground caps nesting at 20 and documents the cap in its own UI.

The pipeline

From query string to ListResponse

Every stage runs in this page. The provider gate models the documented subsets; the parser enforces the grammar and the depth cap; the evaluator applies the Section 3.4.2.2 semantics; the envelope builder follows Section 3.4.

Filter evaluation pipeline: provider subset gate, parser with depth cap, evaluator, and the ListResponse or error envelopeGET /Users?filter=...provider gatedocumented subsetparserFigure 1 + depth capevaluatorcaseExact, absent=falseacceptsASTmatch400 unsupported filter400 invalidFilter200 ListResponsetotalResults, Resources400 tooManyroot query past thresholdempty match: 200 with totalResults 0 - partial pages add startIndex and itemsPerPage (Section 3.4)
The mechanics

What the evaluator does, precisely

Grammar. Figure 1 defines FILTER = attrExp / logExp / valuePath / *1"not" "(" FILTER ")"with the operators eq ne co sw ew pr gt ge lt le, the logical and or not, grouping parentheses, and bracketed value paths over complex attributes. Operators and logical keywords are case-insensitive (Section 3.4.2.2, whose own example writes userName Eq "john"); the comparison literals true, false, and null stay case-sensitive.

Precedence. Attribute expressions are atomic; not binds tighter thanand, and tighter than or, and explicit parentheses override. That is the erratum 4670 reading, and it is what the RFC's examples require. The parse tree in the playground shows the grouping each rule produced.

Evaluation. A multi-valued attribute matches when any value matches. Absent is false: a filter over an unassigned attribute (null, absent, or empty array per RFC 7643 Section 2.5) evaluates to false, and ne over an absent attribute is false too. Cross-type comparisons evaluate as absent rather than erroring. String comparisons follow the attribute's caseExactcharacteristic (RFC 7643 Section 2.2): false, the default, folds both sides. Ordered comparisons against Boolean or Binary attributes fail with invalidFilter (Table 3), which the playground raises as a real error, not a silent false.

The envelope. A query answers 200 with a ListResponse: totalResults is REQUIRED; Resources is REQUIRED when the result set is non-zero; startIndex anditemsPerPage are REQUIRED when the response is partial (Section 3.4, pagination per Section 3.4.2.4). An empty match is a 200 with totalResults: 0. At the server root, a match count past the server's threshold is rejected with 400 tooMany instead of returning the set (Section 3.4.2.1); the threshold control reproduces that contract.

Provider subsets. The toggles read the provider dataset this lab-site publishes: documented operator subsets and matching-attribute allowlists (Entra, AWS, GitHub, Zoom, Atlassian, and Okta's userName eq requirement). A provider rejection is not an RFC violation; it is the documented, narrower contract an integration must model before the first request goes out.

Open questions

Three errata the grammar carries

These are recorded disputes about the published text, not settled corrections. Each card states what the text says, what the erratum disputes, and what this playground implements; the playground's behavior is the teachable position, and reasonable implementations differ.

Space between "not" and "(" erratum 7319, reported (3.4.2.2)

As published: The ABNF `*1"not" "(" FILTER ")"` allows no whitespace between the word "not" and the opening parenthesis.

The dispute: The RFC's own example filters write `not (emails co "example" ...)`. Erratum 7319 (Reported, 2023-01-25) proposes `*1("not" SP)`; alternatives in discussion include optional whitespace or fixing the examples instead.

This playground implements: Whitespace between "not" and "(" is accepted, so both the ABNF form and the example form parse.

erratum 7319 on rfc-editor.org

Parenthesized value filters erratum 7322, reported (3.4.2.2)

As published: No production in Figure 1 allows a bare parenthesized group: the only parenthesized form is `*1"not" "(" ... ")"`, so `emails[type eq "work" or (type eq "home" and value ew "@example.com")]` has no valid parse.

The dispute: Table 5 grants "( )" precedence grouping, and the section's examples use it. Erratum 7322 (Reported, 2023-01-25) adds a recursive valLogExp to valFilter, correcting held erratum 4690.

This playground implements: Parenthesized grouping is accepted inside value filters and at the top level, per Table 5 and the examples.

erratum 7322 on rfc-editor.org

Evaluation-order precedence erratum 4670, held (3.4.2.2)

As published: The prose orders evaluation "grouping, then logical operators, then attribute operators", listing logical operators as binding tighter than attribute operators.

The dispute: Erratum 4670 (Held for Document Update, 2025-10-28) argues attribute operators must bind tighter; taken literally, the published order makes `userName eq "x" and userType pr` unparseable as units.

This playground implements: Attribute expressions are atomic; "not" binds tighter than "and", "and" tighter than "or", and explicit parentheses override. This is the reading the RFC's examples and deployed implementations follow.

erratum 4670 on rfc-editor.org

Attacks and defenses

What each defense is for

Attack or failureDefenseWhere it is defined
Deeply nested filters driving parser recursion to stack exhaustionA depth cap on parsing; this playground rejects beyond 20 and says so in its UIthe parser's own contract; the CVE class in keycloak/keycloak #50472, linagora/scim2 #471
Unselective filters (userName pr) forcing full-directory evaluationThe tooMany rejection past a server-chosen thresholdRFC 7644 Section 3.4.2.1, Table 9
Personal data in filter URIs, which intermediaries logPOST /.search with the filter in the body; 400 sensitive when a provider refuses URI-borne dataRFC 7644 Sections 7.5.2 and 3.4.3, Table 9
Filters valid per the RFC declined by the provider's documented subsetModel the operator subset and matching-attribute allowlist before the first request; the provider toggles rehearse itprovider documentation, dated in the dataset (Entra, ms.date 2026-09-15; GitHub EMU; Zoom; Okta; AWS; Atlassian)
Case mismatches: filters written as if every attribute folded caseRead caseExact from the schema; the AST view annotates each comparison with the resolved handlingRFC 7643 Section 2.2, RFC 7644 Section 3.4.2.2
References