Skip to content

Approval display: payloads with different digests can render identically #113

Description

@ichbindas

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions