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