From 1d70f995f752bb34875024565ce7b4db1734cc25 Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Fri, 12 Jan 2024 19:45:26 -0500 Subject: [PATCH] Limit decoded result imflation ratio from ABI-encoded data (#4537). --- src.ts/abi/abi-coder.ts | 9 +++++++-- src.ts/abi/coders/abstract-coder.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src.ts/abi/abi-coder.ts b/src.ts/abi/abi-coder.ts index ba45478a1a..b19b443cd2 100644 --- a/src.ts/abi/abi-coder.ts +++ b/src.ts/abi/abi-coder.ts @@ -53,7 +53,7 @@ const paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/); let defaultCoder: null | AbiCoder = null; - +let defaultMaxInflation = 1024; function getBuiltinCallException(action: CallExceptionAction, tx: { to?: null | string, from?: null | string, data?: string }, data: null | BytesLike, abiCoder: AbiCoder): CallExceptionError { let message = "missing revert data"; @@ -206,7 +206,12 @@ export class AbiCoder { decode(types: ReadonlyArray, data: BytesLike, loose?: boolean): Result { const coders: Array = types.map((type) => this.#getCoder(ParamType.from(type))); const coder = new TupleCoder(coders, "_"); - return coder.decode(new Reader(data, loose)); + return coder.decode(new Reader(data, loose, defaultMaxInflation)); + } + + static _setDefaultMaxInflation(value: number): void { + assertArgument(typeof(value) === "number" && Number.isInteger(value), "invalid defaultMaxInflation factor", "value", value); + defaultMaxInflation = value; } /** diff --git a/src.ts/abi/coders/abstract-coder.ts b/src.ts/abi/coders/abstract-coder.ts index 6e46e5ca1c..588a129bd8 100644 --- a/src.ts/abi/coders/abstract-coder.ts +++ b/src.ts/abi/coders/abstract-coder.ts @@ -414,10 +414,17 @@ export class Reader { readonly #data: Uint8Array; #offset: number; - constructor(data: BytesLike, allowLoose?: boolean) { + #bytesRead: number; + #parent: null | Reader; + #maxInflation: number; + + constructor(data: BytesLike, allowLoose?: boolean, maxInflation?: number) { defineProperties(this, { allowLoose: !!allowLoose }); this.#data = getBytesCopy(data); + this.#bytesRead = 0; + this.#parent = null; + this.#maxInflation = (maxInflation != null) ? maxInflation: 1024; this.#offset = 0; } @@ -427,6 +434,21 @@ export class Reader { get consumed(): number { return this.#offset; } get bytes(): Uint8Array { return new Uint8Array(this.#data); } + #incrementBytesRead(count: number): void { + if (this.#parent) { return this.#parent.#incrementBytesRead(count); } + + this.#bytesRead += count; + + // Check for excessive inflation (see: #4537) + assert(this.#maxInflation < 1 || this.#bytesRead <= this.#maxInflation * this.dataLength, `compressed ABI data exceeds inflation ratio of ${ this.#maxInflation } ( see: https:/\/github.com/ethers-io/ethers.js/issues/4537 )`, "BUFFER_OVERRUN", { + buffer: getBytesCopy(this.#data), offset: this.#offset, + length: count, info: { + bytesRead: this.#bytesRead, + dataLength: this.dataLength + } + }); + } + #peekBytes(offset: number, length: number, loose?: boolean): Uint8Array { let alignedLength = Math.ceil(length / WordSize) * WordSize; if (this.#offset + alignedLength > this.#data.length) { @@ -445,12 +467,15 @@ export class Reader { // Create a sub-reader with the same underlying data, but offset subReader(offset: number): Reader { - return new Reader(this.#data.slice(this.#offset + offset), this.allowLoose); + const reader = new Reader(this.#data.slice(this.#offset + offset), this.allowLoose, this.#maxInflation); + reader.#parent = this; + return reader; } // Read bytes readBytes(length: number, loose?: boolean): Uint8Array { let bytes = this.#peekBytes(0, length, !!loose); + this.#incrementBytesRead(length); this.#offset += bytes.length; // @TODO: Make sure the length..end bytes are all 0? return bytes.slice(0, length);