commit c9991b9bf82586699abf35c0b6b2cc25c9eb5bf6
parent 9cf327e0ec4ef49bfc6b0d1f5233f5d09108efb3
Author: Christian Grothoff <christian@grothoff.org>
Date: Tue, 3 Nov 2026 00:04:07 +0100
fix #11848
Diffstat:
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