3.3.5
Security release
This release fixes GHSA-jfrf-557c-963v (High). Everyone decoding CBOR that comes from outside the application should upgrade.
Five paths let a document written by an attacker cost far more than its size, or fail outside the documented InvalidArgumentException contract:
- A —
MapObject::normalize()andIndefiniteLengthMapObject::normalize()were quadratic in the number of entries. A 469 kB map took 45.5 s, and the same cost was paid insidedecode()itself when a map was used as a map key. - B — a decimal fraction (tag 4) or a big float (tag 5) with a large exponent expanded a six-byte item into kilobytes. 6000 such keys, 53.7 kB in all, exhausted a 128 MB limit inside
decode(), with a fatal error notry/catchcan intercept. - C — converting the byte string of a big number (tags 2 and 3) is a base conversion that
brick/mathperforms in time quadratic in the length on every calculator but GMP. On an installation with neitherext-gmpnorext-bcmath— whatcomposer requiregives by default — 504 bytes cost 1.8 s and 2 kB close to a hundred, insidedecode(). - D — a date-time (tag 0) holding a NUL byte raised a
ValueError, which is anError, so a caller guarding the parse withInvalidArgumentExceptionnever caught it. - E — an indefinite-length string with a chunk of the wrong type raised a
RuntimeException, which the documentation reserves for a missing extension.
| Before | After | |
|---|---|---|
A, 100 000-entry map normalize() |
45.5 s | 0.098 s |
A, same map used as a key, inside decode() |
37.6 s | 0.54 s |
B, 6000 tag 4 keys at memory_limit=128M |
fatal OOM, exit 255 | InvalidArgumentException in 0.009 s |
| C, 2 kB big number, no gmp/bcmath | 99 s | rejected, < 1 ms |
| D, E | ValueError / RuntimeException |
InvalidArgumentException |
Behaviour changes
Two documents that used to decode no longer do. Both are deliberate and both raise InvalidArgumentException, so a caller that guards the parse is unaffected:
- a decimal fraction or a big float whose exponent exceeds 1024 in absolute value —
DecimalFractionTag::MAX_ABSOLUTE_EXPONENTandBigFloatTag::MAX_ABSOLUTE_EXPONENT, previously 8192; - a big number wrapping more than 256 bytes, a 2048-bit integer — the new
UnsignedBigIntegerTag::MAX_BYTE_LENGTHandNegativeBigIntegerTag::MAX_BYTE_LENGTH.
An indefinite-length string with an invalid chunk raises InvalidArgumentException rather than RuntimeException.
Install ext-gmp when the CBOR input is untrusted. Without it, big number conversion stays on a slow path; the new length bound keeps its cost proportionate to the document, but GMP is what removes it.
web-auth/webauthn-framework users
No action beyond upgrading. WebAuthn never reaches tags 0, 2, 3, 4 or 5, so neither new bound applies to it, and it catches Throwable. Verified by decoding the 25 attestation objects in the webauthn-framework fixtures — the whole object and the embedded COSE public key — against 3.3.4 and 3.3.5: identical results. The one measurable effect is in its favour: the normalize() AttestationObjectLoader performs on the whole attestation object before any validation is no longer quadratic.
Release Notes for 3.3.5
3.3.5
- Total issues resolved: 7
- Total pull requests resolved: 11
- Total contributors: 1
bug
- 167: perf(decode): read head arguments natively and defer string length heads thanks to @Spomky
- 164: perf(container): defer length heads and read the stream without a file handle thanks to @Spomky
- 163: perf(float): read IEEE 754 payloads with unpack() instead of brick/math thanks to @Spomky
- 162: fix: robustness, encoding-helper, compatibility and documentation fixes thanks to @Spomky
- 161: fix(tag): normalize tags to the value they encode thanks to @Spomky
- 160: chore(ci): harden the CI and release workflows thanks to @Spomky
- 159: Déprécier SelfDescribeCBORTag au profit de CBORTag thanks to @Spomky
- 158: fix: shortest-form tag numbers and bignum mantissas thanks to @Spomky
- 157: fix(map): key the map entries by their key, not by their position thanks to @Spomky
- 156: Résoudre les classes de tags par numéro de tag, pas par position dans la liste thanks to @Spomky
- 155: fix(integer): encode the full 64-bit range in its shortest form thanks to @Spomky