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:
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,
+ ),
};
}