Colm O hEigeartaigh created CXF-9264:
----------------------------------------

             Summary: Replace the hand-written JSON parser
                 Key: CXF-9264
                 URL: https://issues.apache.org/jira/browse/CXF-9264
             Project: CXF
          Issue Type: Improvement
            Reporter: Colm O hEigeartaigh
            Assignee: Colm O hEigeartaigh
             Fix For: 4.3.0


 
h2. Summary
 
Replace the hand-written JSON parser and writer in 
\{{JsonMapObjectReaderWriter}} (cxf-rt-rs-json-basic) with Jakarta JSON-P 
(Eclipse Parsson). Keep the public \{{JsonMapObject}} / 
\{{JsonMapObjectReaderWriter}} API as a thin adapter so that JOSE, OAuth2 and 
OIDC callers are unaffected in the first step.
 
h2. Background
 
{\{JsonMapObjectReaderWriter}} is a hand-written JSON parser and writer (about 
640 LOC). It works on substrings: it finds the matching closing bracket for 
each object or array, slices out that substring, and parses it recursively. It 
sits on the security-critical path for:
* JOSE: JWS/JWE headers, JWT claims, JWK/JWK sets (\{{JoseHeaders}}, 
\{{JwtClaims}}, \{{JsonWebKey}}, \{{JsonWebKeys}}, \{{JwkReaderWriter}}, 
\{{JoseUtils}})
* OAuth2: token requests and responses (\{{OAuthJSONProvider}}), 
\{{ClientRegistration}}, \{{ClientRegistrationResponse}}, 
\{{AuthorizationMetadata}}
* OIDC: \{{ClaimsRequest}}, \{{ClaimRequirement}}, \{{UserAddress}}, and the 
IdP and RP services
* the \{{JsonMapObjectProvider}} JAX-RS provider
* \{{OpenApiParseUtils}} (cxf-rt-rs-description-openapi-v3)
 
About 40 non-test classes depend on it, and 10 classes extend 
\{{JsonMapObject}}.
 
Much of its input comes from untrusted sources: JWT/JWS/JWE tokens, OAuth token 
and registration requests, and JWK sets fetched from remote endpoints.
 
h2. Motivation
 
The parser has needed a steady stream of correctness and hardening fixes, for 
example:
* PR 3137: NaN / infinite values in JSON / JWT
* PR 3140, PR 3167: escaped backslash and quote handling
* PR 3141: key parsing
* PR 3142: unicode characters
* PR 3145: escaping control characters
* PR 3149, PR 3152: depth limit on parsing and writing
* PR 3168: nested arrays
* PR 3172: default limits on the number of object keys and array elements
* CXF-9240 (PR 3367): \{{OAuthJSONProvider}} did not escape JSON string values
 
Every one of these bugs is already handled by a standard, widely used parser. 
Keeping our own means we keep finding and fixing the same class of bugs, and 
every fix lands on the token-processing path.
 
h2. Proposed approach
 
h3. Phase 1: swap the implementation behind the existing API
 
* Re-implement the read paths of \{{JsonMapObjectReaderWriter}} (\{{fromJson}}, 
\{{fromJsonToJsonObject}}, \{{fromJsonAsList}}) with the streaming 
\{{jakarta.json.stream.JsonParser}}. Build the same \{{LinkedHashMap}} / 
\{{List}} / \{{Long}} / \{{Double}} / \{{String}} / \{{Boolean}} / \{{null}} 
structures as today.
* Re-implement the write paths (\{{toJson}}) with 
\{{jakarta.json.stream.JsonGenerator}}.
* Do not change the public API in this phase. Subclasses such as 
\{{JwkReaderWriter}} and all \{{JsonMapObject}} subclasses must keep working 
without modification.
* Add the Jakarta JSON-P API and Parsson dependencies to cxf-rt-rs-json-basic. 
Parsson is already managed in parent/pom.xml (\{{cxf.parsson.version}}).
 
h3. Phase 2: move direct callers off the old internals
 
* Move \{{OAuthJSONProvider}} and \{{JsonMapObjectProvider}} onto the new 
implementation and remove any remaining hand-written escaping.
* Move \{{OpenApiParseUtils}} off json-basic (Jackson / swagger-core are 
already available in that module).
* Remove the protected internals that existed only to support substring parsing 
(\{{internalFromJsonAsList}}, the \{{Settable}} plumbing, \{{getClosingIndex}} 
etc.), deprecating them first if they are part of the API.
 
h3. Phase 3 (optional, separate issue)
 
Decide whether to keep cxf-rt-rs-json-basic as a published module or fold it 
into JOSE as an internal detail. Document that it is not intended as a 
general-purpose parser for application payloads.
 
h2. Behaviour to preserve (must be covered by tests before the swap)
 
These are deliberate behaviours of the current implementation that callers rely 
on:
 
# *Duplicate key detection for JOSE.* The parser fills objects through 
\{{JsonMapObject.setProperty}}, which counts repeated keys in \{{updateCount}}. 
\{{JwsCompactConsumer}} and \{{JweCompactConsumer}} check \{{getUpdateCount()}} 
to reject headers with duplicate keys. The new reader must still route every 
key through \{{setProperty}}, or detect duplicates in some other way. It must 
not silently keep the first or the last value.
# *Number mapping.* Integers that fit are parsed as \{{Long}}, everything else 
as \{{Double}}, and non-finite values are rejected. Claims code (\{{exp}}, 
\{{iat}}, \{{nbf}}) casts to these types, so the types must stay the same. 
Decide and document what happens to integers that don't fit in a \{{long}} 
(today they fall through to \{{Double}} and lose precision; consider rejecting 
them).
# *Key order.* Objects are read into \{{LinkedHashMap}} and written in 
insertion order.
# *Compact output format.* With \{{format=false}} the output must stay 
byte-identical (no whitespace, same escaping), because existing tests compare 
against RFC 7515 / 7516 / 7517 examples and against stored values. Signature 
verification itself uses the raw base64url segments, but serialised headers and 
claims become part of the signing input on the producing side.
# *Pretty-print mode.* \{{new JsonMapObjectReaderWriter(true)}} is used by 
\{{JoseUtils}} for logging and debug output. The exact format may change, but 
it must remain valid JSON.
# *\{{JsonObject}} marker values.* Values implementing \{{JsonObject}} 
(\{{JwsJsonSignatureEntry}}, \{{JweJsonEncryptionEntry}}) are written unquoted 
using their \{{toString()}}, which produces JSON. Keep this, or replace it with 
an explicit serialisation hook.
# *Non-String scalar values.* Other objects are written as quoted 
\{{toString()}}, and today they are *not* escaped (only \{{String}} values 
are). The new writer must escape them as well. This is a deliberate fix, not a 
behaviour change to preserve.
# *Limits.* Keep the current defaults and system properties:
#* nesting depth 500 (on read and write)
#* \{{org.apache.cxf.jaxrs.json.basic.maxObjectKeys}} (default 10,000)
#* \{{org.apache.cxf.jaxrs.json.basic.maxArrayElements}} (default 10,000)
Parsson's built-in limits (depth, big number scale and length) should be 
checked for the version in use and set at least as strictly. Limits that 
Parsson doesn't provide (key and element counts) must be enforced in the 
adapter while streaming.
# *Error type.* Malformed input currently surfaces as \{{UncheckedIOException}} 
/ \{{NumberFormatException}} / \{{IllegalArgumentException}}. Callers in JOSE 
and OAuth translate these into protocol errors (for example \{{JwsException}}, 
\{{OAuthServiceException}}). Map Parsson's \{{JsonParsingException}} to the 
same exceptions so error handling and HTTP status codes don't change.
# *Escape decoding.* Unicode escapes, surrogate pairs and escaped keys must 
decode the same way. All the cases added in the PRs listed above must keep 
passing.
 
h2. Test plan
 
* Before changing anything: add characterisation tests for every item in 
"Behaviour to preserve", run them against the current implementation, and 
commit them first.
* Keep all existing json-basic, JOSE, OAuth2 and OIDC unit tests passing 
unchanged, plus the rs-security systests (jose, oauth2, oidc, jwt).
* Add tests for malformed and adversarial input: truncated documents, deep 
nesting, huge key and array counts, very long numbers, invalid escapes, lone 
surrogates, BOMs and trailing data. Check that each fails with the expected 
exception and bounded resource use.
* Optional: run the RFC 7520 (JOSE cookbook) examples end to end.
* Optional: a microbenchmark comparing old and new parse/serialise for typical 
JWT and JWK payloads, to confirm there's no regression. The current substring 
approach is expected to be slower on large or deeply nested input.
 
h2. Out of scope
 
* Changing the public \{{JsonMapObject}} API or the class hierarchy of JOSE / 
OAuth model classes.
* Replacing the JSON-B / JSON-P / Jackson providers used for application 
payloads.
* Removing the Jettison-based \{{JSONProvider}} (tracked separately).
 
h2. Acceptance criteria
 
* \{{JsonMapObjectReaderWriter}} no longer contains any hand-written tokenizing 
or escaping code. All parsing and generation goes through JSON-P.
* All characterisation, unit and systests pass, and compact serialisation of 
existing JOSE test vectors is byte-identical.
* The duplicate-header rejection in JOSE still works (covered by a test).
* The existing limits and system properties still work and are documented.
* Release notes and the migration guide mention the new dependency 
(jakarta.json-api and Parsson in cxf-rt-rs-json-basic), along with any 
intentional behaviour changes (escaping of non-String scalars, handling of 
integers that don't fit in a \{{long}}).
 
h2. Suggested sub-tasks
 
# Characterisation tests for current JsonMapObjectReaderWriter behaviour
# JSON-P based reader behind the existing API (including duplicate-key routing 
and limits)
# JSON-P based writer behind the existing API (byte-identical compact output)
# Move OAuthJSONProvider and JsonMapObjectProvider onto the new implementation
# Move OpenApiParseUtils off json-basic
# Deprecate / remove the substring-parsing internals
# Documentation: release notes, migration guide, THREAT_MODEL.md note



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to