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:
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)}`);
},