taler-typescript-core

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

commit 52eb98d4e71971d5f6b4fb82d90f0ea19d18322d
parent 9718a3bfddc58eacbcaf2af3aa8fc05c255004ce
Author: Florian Dold <dold@taler.net>
Date:   Sun, 13 Sep 2026 16:16:59 +0200

taler-util: reject dot-segment API identifiers

Reject exact dot and double-dot identifiers before constructing an API
request URL. URL parsers normalize these segments even when percent-
encoded, so escaping alone cannot keep them within the intended resource
path.

Diffstat:
Mpackages/taler-util/src/http-client/path-escaping.test.ts | 27++++++++++++++++++++++++++-
Mpackages/taler-util/src/http-client/utils.ts | 5+++++
2 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/packages/taler-util/src/http-client/path-escaping.test.ts b/packages/taler-util/src/http-client/path-escaping.test.ts @@ -53,7 +53,8 @@ class RecordingHttpLib implements HttpRequestLibrary { test("pathSegment escapes separators and traversal", (t) => { assert.strictEqual(pathSegment("alice"), "alice"); - assert.strictEqual(pathSegment(".."), ".."); + assert.throws(() => pathSegment(".."), TypeError); + assert.throws(() => pathSegment("."), TypeError); assert.strictEqual(pathSegment("a/b"), "a%2Fb"); assert.strictEqual(pathSegment("../admin"), "..%2Fadmin"); assert.strictEqual(pathSegment("a?b"), "a%3Fb"); @@ -62,6 +63,30 @@ test("pathSegment escapes separators and traversal", (t) => { assert.strictEqual(pathSegment(42), "42"); }); +test("dot identifiers are rejected before merchant or bank requests", async () => { + for (const id of [".", ".."]) { + const lib = new RecordingHttpLib(); + const merchant = new TalerMerchantInstanceHttpClient( + "https://merchant.example/api/", + lib, + ); + const bank = new TalerCoreBankHttpClient("https://bank.example/api/", lib); + await assert.rejects( + merchant.getOrderDetails("token" as any, id), + TypeError, + ); + await assert.rejects( + merchant.claimOrder({ orderId: id, body: { nonce: "nonce" } }), + TypeError, + ); + await assert.rejects( + bank.getTransactions({ username: id, token: "token" as any }), + TypeError, + ); + assert.strictEqual(lib.lastUrl, undefined); + } +}); + test("a hostile username cannot escape its path segment", async (t) => { // new URL("accounts/" + "../admin" + "/transactions", base) resolves the // dot segment and eliminates "accounts/" entirely. diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts @@ -35,8 +35,13 @@ import { * * new URL() resolves structure rather than escaping it, so an unescaped "/" * injects extra segments and ".." eliminates the segment before it. + * Reject exact dot segments: encodeURIComponent leaves them unchanged, and + * URL parsers normalize even percent-encoded dot segments. */ export function pathSegment(x: string | number): string { + if (x === "." || x === "..") { + throw new TypeError("dot segments are not valid API path identifiers"); + } return encodeURIComponent(x); }