ADR 0012: A lone UTF-16 surrogate makes a request malformed and a ruleset invalid
Status: accepted
Status: accepted
Context
JSON can carry a UTF-16 code unit from D800 to DFFF that is not part of a pair, as an escape
("\ud800"). It is not a code point, and the runtimes read it differently: Go's encoding/json
replaced it with U+FFFD, while Python, JavaScript and Java kept it. So len, matches and equality
could answer the same request differently in two runtimes, which breaks "evaluated identically". The
Python compiler also crashed on a member name holding one, while computing the checksum. I-JSON
(RFC 7493) already forbids such strings.
Decision
- Refuse it. A string or member name holding a lone surrogate, at any depth in a member that
specification section 8 lists, makes the request malformed:
RequestError(400) in every runtime andBAD_REQUESTfrom an engine. It is checked after the shape and depth checks and before anything is evaluated. Members a request does not define stay ignored. - A ruleset holding one does not load (
SCHEMA_INVALID, checked once the schema passes), for the child and for its parent. - A surrogate pair is one character, as before.
- Go keeps the escape in
ParseJSON.encoding/jsonturns a lone surrogate into U+FFFD before any check can see it, soParseJSONkeeps it as the three bytes UTF-8 would give that code unit (bytes valid UTF-8 never contains), the checks look for those bytes, andMarshalwrites them back as the escape.
Consequences
- Every runtime refuses the same input at the same point; conformance has 7 protocol cases and 4 load-error cases for it.
- A client that sent a lone surrogate and got an answer now gets a 400. Released while the packages are alpha.
- Go only refuses it when the JSON was read with
ParseJSON. A Go caller that decodes withencoding/json(orjson.Decoder.UseNumber()) already holds U+FFFD, so the request is accepted and evaluated with U+FFFD. The Go README and therules-cascade-goskill say thatParseJSONis required for this guarantee. - The Go JSON layer has a slower path for string tokens that contain a
\ud/\uDescape; other strings take the same path as before.
Alternatives considered
- Replace it with U+FFFD everywhere. Identical results, but the engine would evaluate data the caller did not send, and a rule would see a character the client never typed.
- Leave it undefined. No work, but the runtimes keep disagreeing, and the conformance suite could not say which answer is right.
ADR 0011: The 422 answer shows only the rules on the client channel
Status: accepted
Security
Please report it privately by email to support@rulescascade.com, never in a public forum. Include the affected package and version, a minimal ruleset or request that shows the problem, and what an attacker gains.