diff --git a/library/helpers/extractStringsFromUserInput.test.ts b/library/helpers/extractStringsFromUserInput.test.ts index 3c0e08dc0..2880c669e 100644 --- a/library/helpers/extractStringsFromUserInput.test.ts +++ b/library/helpers/extractStringsFromUserInput.test.ts @@ -233,3 +233,32 @@ t.test("it handles deeply nested JWT without stack overflow", async () => { t.ok(result.size > 0); t.ok(result.has("Test'), ('Test2');--")); }); + +t.test("it decodes buffers and array buffers", async () => { + const buffer = Buffer.from("Hello, world!", "utf-8"); + const arrayBuffer = buffer.buffer.slice( + buffer.byteOffset, + buffer.byteOffset + buffer.byteLength + ); + + t.same( + extractStringsFromUserInput({ + buf: buffer, + arrBuf: arrayBuffer, + }), + fromArr(["buf", "Hello, world!", "arrBuf", "Hello, world!"]) + ); +}); + +t.test("it ignores large buffers and array buffers", async () => { + process.env.AIKIDO_MAX_BODY_SIZE_MB = "1"; // 1 MB + const largeBuffer = Buffer.alloc(1024 * 1024 + 1, "a"); // 1 MB + 1 byte + + t.same( + extractStringsFromUserInput({ + buf: largeBuffer, + }), + fromArr(["buf"]) + ); + delete process.env.AIKIDO_MAX_BODY_SIZE_MB; +}); diff --git a/library/helpers/extractStringsFromUserInput.ts b/library/helpers/extractStringsFromUserInput.ts index 9e7fc7b13..8036c4961 100644 --- a/library/helpers/extractStringsFromUserInput.ts +++ b/library/helpers/extractStringsFromUserInput.ts @@ -1,6 +1,8 @@ +import { getMaxBodySize } from "./getMaxBodySize"; import { isPlainObject } from "./isPlainObject"; import { safeDecodeURIComponent } from "./safeDecodeURIComponent"; import { tryDecodeAsJWT } from "./tryDecodeAsJWT"; +import { tryDecodeBuffer } from "./tryDecodeBuffer"; type UserString = string; @@ -66,5 +68,14 @@ export function extractStringsFromUserInput( } } + if (Buffer.isBuffer(obj) || obj instanceof ArrayBuffer) { + if (obj.byteLength < getMaxBodySize()) { + const decoded = tryDecodeBuffer(obj); + if (decoded) { + results.add(decoded); + } + } + } + return results; } diff --git a/library/helpers/tryDecodeBuffer.test.ts b/library/helpers/tryDecodeBuffer.test.ts new file mode 100644 index 000000000..a3548e3fe --- /dev/null +++ b/library/helpers/tryDecodeBuffer.test.ts @@ -0,0 +1,46 @@ +import * as t from "tap"; +import { tryDecodeBuffer } from "./tryDecodeBuffer"; + +t.test("tryDecodeBuffer decodes valid UTF-8 buffer", async (t) => { + const buffer = Buffer.from("Hello, world!", "utf-8"); + const result = tryDecodeBuffer(buffer); + t.equal(result, "Hello, world!"); +}); + +t.test( + "tryDecodeBuffer returns undefined for invalid UTF-8 buffer", + async (t) => { + const buffer = Buffer.from([0xff, 0xfe, 0xfd]); + const result = tryDecodeBuffer(buffer); + t.equal(result, undefined); + } +); + +t.test("tryDecodeBuffer decodes valid UTF-8 ArrayBuffer", async (t) => { + const encoder = new TextEncoder(); + const uint8Array = encoder.encode("Hello, ArrayBuffer!"); + const arrayBuffer = uint8Array.buffer; + const result = tryDecodeBuffer(arrayBuffer); + t.equal(result, "Hello, ArrayBuffer!"); +}); + +t.test( + "tryDecodeBuffer returns undefined for invalid UTF-8 ArrayBuffer", + async (t) => { + const invalidArray = new Uint8Array([0xff, 0xfe, 0xfd]); + const result = tryDecodeBuffer(invalidArray.buffer); + t.equal(result, undefined); + } +); + +t.test("tryDecodeBuffer with different encoding", async (t) => { + const buffer = Buffer.from("48656c6c6f", "hex"); // "Hello" in hex + const result = tryDecodeBuffer(buffer, "utf-8", false); + t.equal(result, "Hello"); +}); + +t.test("tryDecodeBuffer with utf8Only false", async (t) => { + const buffer = Buffer.from([0xff, 0xfe, 0xfd]); + const result = tryDecodeBuffer(buffer, "utf-8", false); + t.equal(result, "���"); // Should decode to replacement characters +}); diff --git a/library/helpers/tryDecodeBuffer.ts b/library/helpers/tryDecodeBuffer.ts new file mode 100644 index 000000000..cb4ca8b05 --- /dev/null +++ b/library/helpers/tryDecodeBuffer.ts @@ -0,0 +1,15 @@ +export function tryDecodeBuffer( + data: Buffer | ArrayBuffer, + encoding = "utf-8", + fatal = true +): string | undefined { + try { + const decoder = new TextDecoder(encoding, { + fatal: fatal, // Throw error if buffer is not matching the encoding + }); + + return decoder.decode(data); + } catch { + return undefined; + } +}