taler-typescript-core

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

commit e3b2f049795121bcd4e99cf07f221f0207cdac21
parent 0989504aa59c5e6911a7f4b862782e8635ba2b8c
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:06:20 +0200

util: keep control characters out of generated payment QR codes

The EPC and Swiss records are newline delimited and interpolate payto
parameters, which the parser percent-decodes, so a receiver-name containing
%0A made the beneficiary IBAN of the code attacker-chosen.

Diffstat:
Mpackages/taler-util/src/qr.ts | 27++++++++++++++++++++-------
1 file changed, 20 insertions(+), 7 deletions(-)

diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts @@ -24,6 +24,19 @@ import { Result } from "./result.js"; type EncodeResult = { type: "ok"; qrContent: string } | { type: "skip" }; /** + * Both record formats are newline delimited, so a value carrying a line break + * would shift every field after it -- including the beneficiary account. No + * field of either format may contain a control character anyway. + */ +function qrField(value: string | undefined): string { + if (value == null) { + return ""; + } + // eslint-disable-next-line no-control-regex + return value.replace(/[\u0000-\u001f\u007f]/g, " "); +} + +/** * See "Schweizer Implementation Guidelines QR-Rechnung". */ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { @@ -55,11 +68,11 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { beneficiaryIban, // Beneficiary IBAN // Group: Beneficiary "S", // Address type (S: structured) - parsedPayto.params["receiver-name"], // Beneficiary name + qrField(parsedPayto.params["receiver-name"]), // Beneficiary name "", // street "", // apt. nr. - parsedPayto.params["receiver-postal-code"], // town, // postal code - parsedPayto.params["receiver-town"], // town + qrField(parsedPayto.params["receiver-postal-code"]), // town, // postal code + qrField(parsedPayto.params["receiver-town"]), // town countryCode, // Country // Group: Ultimate Debtor (not used in version 0200) "", // Ultimate Debtor Address type (S: structured) @@ -82,9 +95,9 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { "", // Debtor country // Group: Reference referenceType, // reference type - referenceValue, // Reference + qrField(referenceValue), // Reference // Group: Additional Information - parsedPayto.params["message"], // Unstructured data + qrField(parsedPayto.params["message"]), // Unstructured data "EPD", // End of payment data ]; @@ -129,11 +142,11 @@ function encodePaytoAsEpcQr(paytoUri: string): EncodeResult { "1", // character set (1: UTF-8) "SCT", // Identification "", // optional BIC - parsedPayto.params["receiver-name"], // Beneficiary name + qrField(parsedPayto.params["receiver-name"]), // Beneficiary name beneficiaryIban, // Beneficiary IBAN amt, // Amount (optional) "", // AT-44 Purpose - parsedPayto.params["message"], // AT-05 Unstructured remittance information + qrField(parsedPayto.params["message"]), // AT-05 Unstructured remittance information ]; return { type: "ok",