merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit c9991b9bf82586699abf35c0b6b2cc25c9eb5bf6
parent 9cf327e0ec4ef49bfc6b0d1f5233f5d09108efb3
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue,  3 Nov 2026 00:04:07 +0100

fix #11848

Diffstat:
Msrc/backenddb/iterate_instances.c | 8++++++++
Msrc/backenddb/sql-schema/merchant-0052.sql | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/test_kyc_refresh.py | 0
3 files changed, 127 insertions(+), 0 deletions(-)

diff --git a/src/backenddb/iterate_instances.c b/src/backenddb/iterate_instances.c @@ -160,8 +160,16 @@ lookup_instances_cb (void *cls, { GNUNET_break (0); lic->qs = GNUNET_DB_STATUS_HARD_ERROR; + GNUNET_PQ_cleanup_result (rs); return; } + if ( (NULL != is.website) && + (! TALER_is_web_url (is.website)) ) + { + GNUNET_break (0); /* merchant-0052 migration should have + prevented this... */ + GNUNET_free (is.website); + } lic->cb (lic->cb_cls, &merchant_pub, (no_priv) ? NULL : &merchant_priv, diff --git a/src/backenddb/sql-schema/merchant-0052.sql b/src/backenddb/sql-schema/merchant-0052.sql @@ -172,4 +172,123 @@ INSERT INTO merchant.instance_fixups -- Apply new fix-up to existing instances CALL merchant.fixup_instance_schema (52::INT8); + +-- Since commit 9a4407f7a in the exchange, the REST input that writes an +-- instance website goes through TALER_JSON_spec_web_url(), i.e. through +-- TALER_is_web_url(), which demands +-- +-- * a case insensitive 'http://' or 'https://' prefix, and +-- * every character drawn from the ASCII set +-- 'A..Za..z0..9' plus '/' ':' ';' '&' '?' '-' '.' ',' '=' '_' '~' '%' '+' '#'. +-- +-- An older backend stored whatever a JSON string could hold, so legacy rows +-- may carry a bare host ('www.example.com'), a foreign scheme, or bytes +-- outside that set. +-- +-- We force the scheme to be 'https://'. That's a best-case guess and +-- could be wrong, just like the user could have specified something +-- other than a domain name under 'website'. Tough luck, best-effort +-- migration. +-- +-- If we detect a value that names some *other* scheme, we just +-- drop it. Instance owner can easily set their website later, better than +-- us silently building nonsense like 'https://ftp://example.com'. +-- Warnings are logged when we set a website to NULL. + +SET search_path TO merchant; + +DO $$ +DECLARE + -- These two are the Web URL predicate, and must stay in sync with + -- TALER_is_web_url() in the exchange (src/util/url.c). They are defined + -- once: both the repair below and the CHECK constraint at the end are + -- built from these variables, so the two cannot drift apart. Keep the + -- constraint no stricter than the C check, or the backend would accept + -- input that the database then refuses with a 500. + my_scheme_re TEXT DEFAULT '^https?://'; + my_charset_re TEXT DEFAULT '^[A-Za-z0-9/:;&?.,=_~%+#-]+$'; + my_new_scheme TEXT DEFAULT 'https://'; + my_conname TEXT DEFAULT 'merchant_instances_website_web_url_check'; + rec RECORD; + my_trimmed TEXT; + my_new TEXT; +BEGIN + FOR rec IN + SELECT merchant_serial + ,merchant_id + ,website + FROM merchant.merchant_instances + WHERE website IS NOT NULL + AND NOT ( (website ~* my_scheme_re) + AND (website ~ my_charset_re) ) + FOR UPDATE + LOOP + -- Surrounding whitespace is not in the allowed set and would otherwise + -- cost us the whole URL, while the intent is unambiguous. + my_trimmed := btrim(rec.website, E' \t\n\r\f\v'); + + IF my_trimmed = '' + THEN + my_new := NULL; + ELSIF my_trimmed ~* my_scheme_re + THEN + -- Scheme is already right, so only the character set can be at + -- fault; the final guard below rejects it. + my_new := my_trimmed; + ELSIF (my_trimmed ~ '^[A-Za-z][A-Za-z0-9+.-]*:') + AND (my_trimmed !~ '^[A-Za-z0-9.-]+:[0-9]') + THEN + -- Bad character set + my_new := NULL; + ELSE + my_new := my_new_scheme || my_trimmed; + END IF; + + -- Extra guard: satisfy exactly the predicate the constraint enforces + IF (my_new IS NOT NULL) + AND NOT ( (my_new ~* my_scheme_re) + AND (my_new ~ my_charset_re) ) + THEN + my_new := NULL; + END IF; + + IF my_new IS DISTINCT FROM rec.website + THEN + UPDATE merchant.merchant_instances + SET website=my_new + WHERE merchant_serial=rec.merchant_serial; + + IF my_new IS NULL + THEN + RAISE WARNING + 'instance %: website dropped, it cannot be repaired automatically', + rec.merchant_id; + ELSE + RAISE NOTICE + 'instance %: website normalized to %', + rec.merchant_id, my_new; + END IF; + END IF; + END LOOP; + + -- Enforce the invariant from here on. A NULL website stays legal: the + -- column is optional, and CHECK accepts NULL. + PERFORM + FROM pg_constraint c + JOIN pg_namespace n + ON (n.oid = c.connamespace) + WHERE n.nspname = 'merchant' + AND c.conname = my_conname; + IF NOT FOUND + THEN + EXECUTE format('ALTER TABLE merchant.merchant_instances' + ' ADD CONSTRAINT %I' + ' CHECK ( (website ~* %L)' + ' AND (website ~ %L) )', + my_conname, + my_scheme_re, + my_charset_re); + END IF; +END $$; + COMMIT; diff --git a/src/backenddb/test_kyc_refresh.py b/src/backenddb/test_kyc_refresh.py