commit 4d22292628163e1268ad1579150722c398d13e35
parent 66835ab57e8c1710532a3cb9f622b9077e902047
Author: Florian Dold <dold@taler.net>
Date: Mon, 20 Jul 2026 13:58:59 +0200
util: improve (JSON) amount input validation
Diffstat:
2 files changed, 87 insertions(+), 4 deletions(-)
diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts
@@ -17,7 +17,12 @@
import { test } from "node:test";
import assert from "node:assert";
-import { Amounts, AmountJson, amountMaxValue } from "./amounts.js";
+import {
+ Amounts,
+ AmountJson,
+ amountMaxValue,
+ codecForAmountJson,
+} from "./amounts.js";
import { AmountString } from "./types-taler-common.js";
const jAmt = (
@@ -126,6 +131,39 @@ test("amount parsing", (t) => {
assert.throws(() => Amounts.parseOrThrow("TESTKUDOS:0.999999991"));
});
+test("Amounts.parseOrThrow rejects out-of-range or non-integer object fields", (t) => {
+ // baseline: a well-formed object round-trips fine.
+ assert.deepStrictEqual(Amounts.parseOrThrow(jAmt(1, 2, "EUR")), {
+ currency: "EUR",
+ value: 1,
+ fraction: 2,
+ });
+
+ assert.throws(() => Amounts.parseOrThrow(jAmt(-1, 0, "EUR")));
+ assert.throws(() => Amounts.parseOrThrow(jAmt(1.5, 0, "EUR")));
+ assert.throws(() => Amounts.parseOrThrow(jAmt(amountMaxValue + 1, 0, "EUR")));
+ assert.throws(() => Amounts.parseOrThrow(jAmt(0, -1, "EUR")));
+ assert.throws(() => Amounts.parseOrThrow(jAmt(0, 1.5, "EUR")));
+ assert.throws(() => Amounts.parseOrThrow(jAmt(0, 100000000, "EUR")));
+});
+
+test("codecForAmountJson rejects out-of-range or non-integer fields", (t) => {
+ const codec = codecForAmountJson();
+ assert.deepStrictEqual(
+ codec.decode({ currency: "EUR", value: 1, fraction: 2 }),
+ { currency: "EUR", value: 1, fraction: 2 },
+ );
+ assert.throws(() =>
+ codec.decode({ currency: "EUR", value: -1, fraction: 0 }),
+ );
+ assert.throws(() =>
+ codec.decode({ currency: "EUR", value: 0, fraction: 100000000 }),
+ );
+ assert.throws(() =>
+ codec.decode({ currency: "EUR", value: 1.5, fraction: 0 }),
+ );
+});
+
test("amount stringification", (t) => {
assert.strictEqual(
Amounts.stringify(jAmt(0, 0, "TESTKUDOS")),
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
@@ -26,7 +26,6 @@ import {
Context,
DecodingError,
buildCodecForObject,
- codecForNumber,
codecForString,
renderContext,
} from "./codec.js";
@@ -135,11 +134,47 @@ export class Amount {
) {}
}
+function codecForAmountValueNumber(): Codec<number> {
+ return {
+ decode(x: any, c?: Context): number {
+ if (
+ typeof x === "number" &&
+ Number.isInteger(x) &&
+ x >= 0 &&
+ x <= amountMaxValue
+ ) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected valid amount value at ${renderContext(c)} but got ${JSON.stringify(x)}`,
+ );
+ },
+ };
+}
+
+function codecForAmountFractionNumber(): Codec<number> {
+ return {
+ decode(x: any, c?: Context): number {
+ if (
+ typeof x === "number" &&
+ Number.isInteger(x) &&
+ x >= 0 &&
+ x < amountFractionalBase
+ ) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected valid amount fraction at ${renderContext(c)} but got ${JSON.stringify(x)}`,
+ );
+ },
+ };
+}
+
export const codecForAmountJson = (): Codec<AmountJson> =>
buildCodecForObject<AmountJson>()
.property("currency", codecForString())
- .property("value", codecForNumber())
- .property("fraction", codecForNumber())
+ .property("value", codecForAmountValueNumber())
+ .property("fraction", codecForAmountFractionNumber())
.build("AmountJson");
export function codecForAmountString(): Codec<AmountString> {
@@ -567,6 +602,16 @@ export class Amounts {
if (typeof s.fraction !== "number") {
throw Error("invalid amount object");
}
+ if (!Number.isInteger(s.value) || s.value < 0 || s.value > amountMaxValue) {
+ throw Error("invalid amount object (value out of range)");
+ }
+ if (
+ !Number.isInteger(s.fraction) ||
+ s.fraction < 0 ||
+ s.fraction >= amountFractionalBase
+ ) {
+ throw Error("invalid amount object (fraction out of range)");
+ }
return { currency: s.currency, value: s.value, fraction: s.fraction };
} else if (typeof s === "string") {
const res = Amounts.parse(s);