Findings from a systematic pass over typedDataDisplay.js. Not blocking #112 — all of these predate the shared-library integration and none is a regression from it.
Each was verified against the real flattenTypedMessage and hashTypedDataHex. In every case both payloads are valid, both hash, and both pass checkPolicyLimits.
1. A string value can fabricate a row
isUnsafeC0ControlCodePoint whitelists \n, and U+2028/U+2029 are untouched, so a leaf can contain text shaped like a row.
A: OrderA [{note,string},{amount,uint64}] message {note:"Confirm refund of $5", amount:5}
B: OrderB [{note,string}] message {note:"Confirm refund of $5\namount : uint64 = 5"}
Different digests. Any line-joined rendering is byte-identical:
note : string = Confirm refund of $5
amount : uint64 = 5
B has no amount field. Suggest flagging embedded line separators the way bidi controls are flagged, and not treating joined text as a canonical form.
2. The row cap hides divergent content behind a count
Flat.amounts: uint64[210], elements 0–199 identical, elements 200–209 differing. Different digests, byte-identical rendering, both ending in {omittedCount: 10, depthLimited: false}. The ten divergent values are never shown. checkPolicyLimits passes both — 210 elements is within the 256 floor.
3. String truncation hides a divergent tail
Note.text = 2048 shared characters plus a differing suffix. Different digests, byte-identical rendering, both flagged truncated. The flag says something was cut, not what. checkPolicyLimits passes both.
4. Invisible characters and normalisation
Zero-width space, ZWNJ/ZWJ, word joiner, BOM and soft hyphen fall outside both the bidi set and the C0/C1 set and pass through unchanged, so "Alice" and "Ali\u200Bce" sign differently and look identical. Same for NFC vs NFD — no normalisation is applied.
5. flattenTypedMessage performs no validation of its own
It rendered a dotted field name as a path segment when called directly. Path ambiguity is currently prevented only because production calls it after hashing has validated. Not exploitable today; it becomes so the first time the function is reused ahead of validation. Escaping and flagging non-identifier name segments would fit the module's existing approach better than throwing, which would conflict with its deliberate tolerance of missing and wrong-typed values.
Not found
Path ambiguity through field or type names is blocked by the IDENT rule in checkFieldDefs. Row ordering cannot diverge from meaning, since declaration order is what encodeType hashes. uint64 given as a number or a decimal string renders identically because parseUint canonicalises both to the same bytes. Truncation cannot split a surrogate pair.
The proposed normative rules behind these are in dusk-network/typed-data#4.
Findings from a systematic pass over
typedDataDisplay.js. Not blocking #112 — all of these predate the shared-library integration and none is a regression from it.Each was verified against the real
flattenTypedMessageandhashTypedDataHex. In every case both payloads are valid, both hash, and both passcheckPolicyLimits.1. A string value can fabricate a row
isUnsafeC0ControlCodePointwhitelists\n, and U+2028/U+2029 are untouched, so a leaf can contain text shaped like a row.Different digests. Any line-joined rendering is byte-identical:
B has no
amountfield. Suggest flagging embedded line separators the way bidi controls are flagged, and not treating joined text as a canonical form.2. The row cap hides divergent content behind a count
Flat.amounts: uint64[210], elements 0–199 identical, elements 200–209 differing. Different digests, byte-identical rendering, both ending in{omittedCount: 10, depthLimited: false}. The ten divergent values are never shown.checkPolicyLimitspasses both — 210 elements is within the 256 floor.3. String truncation hides a divergent tail
Note.text= 2048 shared characters plus a differing suffix. Different digests, byte-identical rendering, both flaggedtruncated. The flag says something was cut, not what.checkPolicyLimitspasses both.4. Invisible characters and normalisation
Zero-width space, ZWNJ/ZWJ, word joiner, BOM and soft hyphen fall outside both the bidi set and the C0/C1 set and pass through unchanged, so
"Alice"and"Ali\u200Bce"sign differently and look identical. Same for NFC vs NFD — no normalisation is applied.5.
flattenTypedMessageperforms no validation of its ownIt rendered a dotted field name as a path segment when called directly. Path ambiguity is currently prevented only because production calls it after hashing has validated. Not exploitable today; it becomes so the first time the function is reused ahead of validation. Escaping and flagging non-identifier name segments would fit the module's existing approach better than throwing, which would conflict with its deliberate tolerance of missing and wrong-typed values.
Not found
Path ambiguity through field or type names is blocked by the
IDENTrule incheckFieldDefs. Row ordering cannot diverge from meaning, since declaration order is whatencodeTypehashes.uint64given as a number or a decimal string renders identically becauseparseUintcanonicalises both to the same bytes. Truncation cannot split a surrogate pair.The proposed normative rules behind these are in dusk-network/typed-data#4.