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.
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 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.
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.
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.
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.
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.
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.
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.
What each defense is for
| Attack or failure | Defense | Where it is defined |
|---|---|---|
| Deeply nested filters driving parser recursion to stack exhaustion | A depth cap on parsing; this playground rejects beyond 20 and says so in its UI | the parser's own contract; the CVE class in keycloak/keycloak #50472, linagora/scim2 #471 |
| Unselective filters (userName pr) forcing full-directory evaluation | The tooMany rejection past a server-chosen threshold | RFC 7644 Section 3.4.2.1, Table 9 |
| Personal data in filter URIs, which intermediaries log | POST /.search with the filter in the body; 400 sensitive when a provider refuses URI-borne data | RFC 7644 Sections 7.5.2 and 3.4.3, Table 9 |
| Filters valid per the RFC declined by the provider's documented subset | Model the operator subset and matching-attribute allowlist before the first request; the provider toggles rehearse it | provider 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 case | Read caseExact from the schema; the AST view annotates each comparison with the resolved handling | RFC 7643 Section 2.2, RFC 7644 Section 3.4.2.2 |
- RFC 7644: System for Cross-domain Identity Management: ProtocolHunt et al. (IETF, September 2015) - the normative text; Section 3.4.2.2 defines the filter grammar and evaluation, Section 3.4 the ListResponse, Section 3.12 the error table
- RFC 7643: SCIM: Core SchemaSection 2.2 assigns the characteristic defaults (caseExact among them); Section 2.5 defines unassigned; Sections 8.2-8.4 are the sample resources this playground evaluates against
- Erratum 7319, 7322, 4670the three open grammar disputes, taught as open questions above; dates from the RFC Editor errata pages, retrieved 2026-11-01
- Erratum 6010the ServiceProviderConfig pagination example that returned itemsPerPage 10 for a 2-item page; the envelope note in the playground cites it
- Microsoft Entra provisioning tutorial, GitHub EMU SCIM reference, Zoom SCIM2 reference, Okta SCIM concepts, AWS IAM Identity Center limitations, Atlassian user provisioning APIthe provider subset sources, all accessed 2026-11-01; the toggles read this lab-site's provider dataset built from them
- scim2-parse-filterthe maintained npm filter parser, credited as prior art; it implements none of the errata readings this playground documents, which is why the parser here is local
- scimmyjs/scimmy #64 (December 2024) and #42 (August 2024); python-scim/scim2-models #88 (March 2025); keycloak/keycloak #50472 (June 2026); linagora/scim2 #471 (October 2026)the real-world ambiguity and CVE record, from the GitHub survey behind this lab-site (2026-11-01)