taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5fed90c02cec29c796e1327ceafd3f9afaa582e1
parent c34007726aac7814dd1fe1bf8978adfc785e6fa5
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 10:58:16 +0200

util: reject malformed numbers in the time codecs

Timestamps and durations are integers per the specification.  NaN was
worst: it serializes back to null and compares as smaller than every other
instant, so an expiry check against it silently inverts.

Diffstat:
Mpackages/taler-util/src/time.ts | 33+++++++++++++++++++++++++++------
1 file changed, 27 insertions(+), 6 deletions(-)

diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts @@ -718,6 +718,23 @@ export function durationAdd(d1: Duration, d2: Duration): Duration { return { d_ms: d1.d_ms + d2.d_ms }; } +/** + * The protocol specification declares every timestamp and duration as an + * Integer, capped at 2^53 - 1. Values outside that -- NaN in particular, + * which serializes back to null and compares as smaller than everything -- + * would otherwise silently propagate into expiry checks. + */ +function checkTimeValue( + n: number, + what: string, + c: Context | undefined, +): number { + if (!Number.isSafeInteger(n) || n < 0) { + throw Error(`expected ${what} at ${renderContext(c)}`); + } + return n; +} + export const codecForAbsoluteTime: Codec<AbsoluteTime> = { decode(x: any, c?: Context): AbsoluteTime { if (x === undefined) { @@ -731,7 +748,10 @@ export const codecForAbsoluteTime: Codec<AbsoluteTime> = { return { t_ms: "never", [opaque_AbsoluteTime]: true }; } } else if (typeof t_ms === "number") { - return { t_ms, [opaque_AbsoluteTime]: true }; + return { + t_ms: checkTimeValue(t_ms, "timestamp", c), + [opaque_AbsoluteTime]: true, + }; } throw Error(`expected timestamp at ${renderContext(c)}`); }, @@ -751,7 +771,7 @@ export const codecForTimestamp: Codec<TalerProtocolTimestamp> = { return { t_s: "never" }; } } else if (typeof t_ms === "number") { - return { t_s: Math.floor(t_ms / 1000) }; + return { t_s: Math.floor(checkTimeValue(t_ms, "timestamp", c) / 1000) }; } const t_s = x.t_s; if (typeof t_s === "string") { @@ -761,7 +781,7 @@ export const codecForTimestamp: Codec<TalerProtocolTimestamp> = { throw Error(`expected timestamp at ${renderContext(c)}`); } if (typeof t_s === "number") { - return { t_s }; + return { t_s: checkTimeValue(t_s, "protocol timestamp", c) }; } throw Error(`expected protocol timestamp at ${renderContext(c)}`); }, @@ -777,12 +797,13 @@ export const codecForPreciseTimestamp: Codec<TalerPreciseTimestamp> = { throw Error(`expected precise timestamp at ${renderContext(c)}`); } if (typeof t_s === "number") { + checkTimeValue(t_s, "precise timestamp", c); const off_us = x.off_us; if (off_us === undefined) { return { t_s }; } if (typeof off_us === "number") { - return { t_s, off_us }; + return { t_s, off_us: checkTimeValue(off_us, "precise timestamp", c) }; } throw Error(`expected precise timestamp at ${renderContext(c)}`); } @@ -801,7 +822,7 @@ export const codecForDuration: Codec<TalerProtocolDuration> = { throw Error(`expected duration at ${renderContext(c)}`); } if (typeof d_us === "number") { - return { d_us }; + return { d_us: checkTimeValue(d_us, "duration", c) }; } throw Error(`expected duration at ${renderContext(c)}`); }, @@ -817,7 +838,7 @@ export const codecForDurationMs: Codec<Duration> = { throw Error(`expected duration at ${renderContext(c)}`); } if (typeof d_ms === "number") { - return { d_ms }; + return { d_ms: checkTimeValue(d_ms, "duration", c) }; } throw Error(`expected duration at ${renderContext(c)}`); },