fix(security): make the CSS sanitizer's remote-URL block actually block

sanitizeCSS "blocked" a remote url() by prefixing it with a
/* BLOCKED URL */ COMMENT and leaving the URL in place. CSS comments are
discarded during tokenization, so the declaration a browser parsed still
carried the live URL — while adminCssTemplates returned
sanitization_warnings claiming it had been stopped. Protection that
reports success is worse than none, which is why it survived review.

Scope is narrow: sanitizeCss (lowercase, the public-site path) never
included the pattern and permits remote URLs by design — a test now pins
that. Only sanitizeCSS (uppercase) was affected; outside this repo's
newsletter branch its sole caller is adminCssTemplates.js.

Migration 200 is required, not cosmetic: gallery.js serves
css_templates.css_content VERBATIM as text/css and does not re-sanitize on
read, so fixing the write path alone would leave every existing template
serving its URL forever.

Review follow-ups replaced the regex with a small three-state lexer
(comment / string / identifier) over the RAW text, after five further
bypasses: a ")" inside a quoted url(), CSS escapes (u\72l), the HTML
comment strip JOINING tokens into a live url() after the scan, an escaped
quote desynchronising the scan, and a quote inside a comment. Escapes are
decoded only to decide, never to rewrite — a clean input now round-trips
byte-identical, which also keeps unaffected rows out of the migration's
write path.

Severity is low (writing a template needs branding.edit) but the harm is
a gallery visitor's IP reaching a third party from a page the operator
believes carries no remote requests.
This commit is contained in:
Paul Nothaft
2026-09-04 14:27:08 +02:00
committed by GitHub
parent de3a7f70bf
commit a7d0972b13
6 changed files with 779 additions and 14 deletions
+4 -2
View File
@@ -3127,7 +3127,8 @@
"bulkDeleteCompleted_one": "Massen-Löschung abgeschlossen: {{count}} Event entfernt",
"bulkDeleteCompleted_other": "Massen-Löschung abgeschlossen: {{count}} Events entfernt",
"bulkArchiveCompleted_one": "Massen-Archivierung abgeschlossen: {{count}} Event archiviert",
"bulkArchiveCompleted_other": "Massen-Archivierung abgeschlossen: {{count}} Events archiviert"
"bulkArchiveCompleted_other": "Massen-Archivierung abgeschlossen: {{count}} Events archiviert",
"cssTemplateRemoteUrlsRemoved": "Externe URLs aus {{count}} CSS-Vorlage(n) entfernt — siehe Release Notes"
},
"notificationToasts": {
"markedAllRead": "Alle Benachrichtigungen als gelesen markiert",
@@ -3429,7 +3430,8 @@
"whatsapp_config_updated": "WhatsApp-Konfiguration aktualisiert",
"feature_flags_summary_one": "{{count}} Funktion aktualisiert: {{summary}}",
"feature_flags_summary_other": "{{count}} Funktionen aktualisiert: {{summary}}",
"feature_flags_updated": "Funktionseinstellungen aktualisiert"
"feature_flags_updated": "Funktionseinstellungen aktualisiert",
"css_template_remote_urls_removed": "Externe URLs aus {{count}} CSS-Vorlage(n) entfernt — siehe Release Notes"
},
"people": {
"title": "Personen in dieser Galerie",
+4 -2
View File
@@ -2657,7 +2657,8 @@
"bulkDeleteCompleted_one": "Bulk delete completed: {{count}} event removed",
"bulkDeleteCompleted_other": "Bulk delete completed: {{count}} events removed",
"bulkArchiveCompleted_one": "Bulk archive completed: {{count}} event archived",
"bulkArchiveCompleted_other": "Bulk archive completed: {{count}} events archived"
"bulkArchiveCompleted_other": "Bulk archive completed: {{count}} events archived",
"cssTemplateRemoteUrlsRemoved": "Remote URLs removed from {{count}} CSS template(s) — see the release notes"
},
"notificationToasts": {
"markedAllRead": "All notifications marked as read",
@@ -2961,7 +2962,8 @@
"whatsapp_config_updated": "WhatsApp configuration updated",
"feature_flags_summary_one": "{{count}} features updated: {{summary}}",
"feature_flags_summary_other": "{{count}} features updated: {{summary}}",
"feature_flags_updated": "Feature settings updated"
"feature_flags_updated": "Feature settings updated",
"css_template_remote_urls_removed": "Remote URLs removed from {{count}} CSS template(s) — see the release notes"
},
"people": {
"title": "People in this gallery",