From 2c173821b909a8ee9b4c334aea6b3a9668db39f4 Mon Sep 17 00:00:00 2001 From: FidelCoder Date: Sun, 17 May 2026 01:09:45 +0300 Subject: [PATCH] fix(core): harden token amount validation --- typescript/packages/core/src/utils/index.ts | 49 ++++++++++++++++++- .../core/test/unit/utils/utils.test.ts | 21 ++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/typescript/packages/core/src/utils/index.ts b/typescript/packages/core/src/utils/index.ts index 5ba2e1ae09..18040bb7fa 100644 --- a/typescript/packages/core/src/utils/index.ts +++ b/typescript/packages/core/src/utils/index.ts @@ -47,7 +47,7 @@ export function convertToTokenAmount(decimalAmount: string, decimals: number): s `Invalid amount: ${decimalAmount} — use decimal notation, not scientific notation`, ); } - if (!/^-?\d+\.?\d*$/.test(decimalAmount)) { + if (!isPlainDecimalAmount(decimalAmount)) { throw new Error(`Invalid amount: ${decimalAmount}`); } const [intPart, decPart = ""] = decimalAmount.split("."); @@ -61,6 +61,53 @@ export function convertToTokenAmount(decimalAmount: string, decimals: number): s return tokenAmount; } +/** + * Validates a plain decimal string with a linear scan to avoid regex backtracking. + */ +function isPlainDecimalAmount(decimalAmount: string): boolean { + if (decimalAmount.length === 0) { + return false; + } + + let index = decimalAmount.startsWith("-") ? 1 : 0; + if (index === decimalAmount.length) { + return false; + } + + let hasIntegerDigit = false; + while (index < decimalAmount.length) { + const charCode = decimalAmount.charCodeAt(index); + if (charCode < 48 || charCode > 57) { + break; + } + hasIntegerDigit = true; + index++; + } + + if (!hasIntegerDigit) { + return false; + } + + if (index === decimalAmount.length) { + return true; + } + + if (decimalAmount[index] !== ".") { + return false; + } + + index++; + while (index < decimalAmount.length) { + const charCode = decimalAmount.charCodeAt(index); + if (charCode < 48 || charCode > 57) { + return false; + } + index++; + } + + return true; +} + /** * Scheme data structure for facilitator storage */ diff --git a/typescript/packages/core/test/unit/utils/utils.test.ts b/typescript/packages/core/test/unit/utils/utils.test.ts index 3b268cafb6..02ae76a8b6 100644 --- a/typescript/packages/core/test/unit/utils/utils.test.ts +++ b/typescript/packages/core/test/unit/utils/utils.test.ts @@ -361,6 +361,11 @@ describe("Utils", () => { expect(convertToTokenAmount("0.1000000", 7)).toBe("1000000"); }); + it("should handle trailing decimal points", () => { + expect(convertToTokenAmount("1.", 6)).toBe("1000000"); + expect(convertToTokenAmount("0.", 6)).toBe("0"); + }); + it("should handle negative numbers", () => { expect(convertToTokenAmount("-1.5", 6)).toBe("-1500000"); }); @@ -420,6 +425,22 @@ describe("Utils", () => { expect(() => convertToTokenAmount("", 6)).toThrow("Invalid amount"); expect(() => convertToTokenAmount("NaN", 6)).toThrow("Invalid amount"); }); + + it("should throw for malformed decimal strings", () => { + expect(() => convertToTokenAmount(".5", 6)).toThrow("Invalid amount"); + expect(() => convertToTokenAmount("+1", 6)).toThrow("Invalid amount"); + expect(() => convertToTokenAmount("-", 6)).toThrow("Invalid amount"); + expect(() => convertToTokenAmount("1.2.3", 6)).toThrow("Invalid amount"); + expect(() => convertToTokenAmount("1 ", 6)).toThrow("Invalid amount"); + }); + + it("should reject long adversarial invalid strings", () => { + const longIntegerWithSuffix = `${"0".repeat(10000)}x`; + const longDecimalWithSuffix = `0.${"0".repeat(10000)}x`; + + expect(() => convertToTokenAmount(longIntegerWithSuffix, 6)).toThrow("Invalid amount"); + expect(() => convertToTokenAmount(longDecimalWithSuffix, 6)).toThrow("Invalid amount"); + }); }); });