taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit d35ff8eaea3975c84fa4585513596b9ecbe9091a
parent 1fbb99d0a221c97a46078d836fb722140b1b7ce4
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:10:27 +0200

util: bound contract decompression by the declared plaintext length

The format tag and the plaintext length in the header were never read, and
zlib was given no output bound, so a peer-authored contract of a couple of
hundred kilobytes expanded to hundreds of megabytes.

Diffstat:
Mpackages/taler-util/src/taler-crypto.ts | 57+++++++++++++++++++++++++++++++++++++++++++--------------
1 file changed, 43 insertions(+), 14 deletions(-)

diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts @@ -1602,6 +1602,39 @@ enum ContractFormatTag { const mergeSalt = "p2p-merge-contract"; const depositSalt = "p2p-deposit-contract"; +/** + * Upper bound on the declared plaintext length of an encrypted contract, + * matching GNUNET_MAX_MALLOC_CHECKED in the C reference. + */ +const maxContractTermsLength = 1024 * 1024 * 40; + +/** + * Read the header an encrypted contract carries ahead of its compressed + * payload and decompress exactly as many bytes as it declares. The blob comes + * from the remote peer, so an unbounded expansion would let it choose how much + * memory we allocate. + */ +function decompressContractTerms( + dec: Uint8Array, + expectedTag: ContractFormatTag, + payloadOffset: number, +): any { + const dv = new DataView(dec.buffer, dec.byteOffset, dec.byteLength); + const ctype = dv.getUint32(0); + const clen = dv.getUint32(4); + if (ctype !== expectedTag) { + throw Error(`unexpected contract format tag ${ctype}`); + } + if (clen < 1 || clen > maxContractTermsLength) { + throw Error(`contract terms length ${clen} out of range`); + } + const buf = fflate.unzlibSync(dec.slice(payloadOffset), { + out: new Uint8Array(clen), + }); + // Slice of the '\0' at the end and decode to a string + return JSON.parse(bytesToString(buf.slice(0, buf.length - 1))); +} + export function encryptContractForMerge( pursePub: PursePublicKey, contractPriv: ContractPrivateKey, @@ -1657,15 +1690,13 @@ export async function decryptContractForMerge( const key = keyExchangeEcdhEddsa(contractPriv, pursePub); const dec = await decryptWithDerivedKey(enc, key, mergeSalt); const mergePriv = dec.slice(8, 8 + 32); - const contractTermsCompressed = dec.slice(8 + 32); - const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); - // Slice of the '\0' at the end and decode to a string - const contractTermsString = bytesToString( - contractTermsBuf.slice(0, contractTermsBuf.length - 1), - ); return { mergePriv: mergePriv, - contractTerms: JSON.parse(contractTermsString), + contractTerms: decompressContractTerms( + dec, + ContractFormatTag.PaymentOffer, + 8 + 32, + ), }; } @@ -1676,14 +1707,12 @@ export async function decryptContractForDeposit( ): Promise<DecryptForDepositResult> { const key = keyExchangeEcdhEddsa(contractPriv, pursePub); const dec = await decryptWithDerivedKey(enc, key, depositSalt); - const contractTermsCompressed = dec.slice(8); - const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed); - // Slice of the '\0' at the end and decode to a string - const contractTermsString = bytesToString( - contractTermsBuf.slice(0, contractTermsBuf.length - 1), - ); return { - contractTerms: JSON.parse(contractTermsString), + contractTerms: decompressContractTerms( + dec, + ContractFormatTag.PaymentRequest, + 8, + ), }; }