From 8653510a85c0a91c77ee6c9908795c8ae685ca85 Mon Sep 17 00:00:00 2001 From: Daniel Vogelheim Date: Tue, 12 May 2026 21:03:34 +0200 Subject: [PATCH 1/4] Refactor "is attribute allowed". --- index.bs | 103 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/index.bs b/index.bs index b2503b0..f13c522 100644 --- a/index.bs +++ b/index.bs @@ -1032,54 +1032,75 @@ beginning with |node|. It consistes of these steps: 1. If |child| is a [=shadow host=], then call [=sanitize core=] on |child|'s [=Element/shadow root=] with |configuration| and |handleJavascriptNavigationUrls|. - 1. Let |elementWithLocalAttributes| be « [] ». - 1. If |configuration|["{{SanitizerConfig/elements}}"] [=map/exists=] and - |configuration|["{{SanitizerConfig/elements}}"] [=SanitizerConfig/contains=] - |elementName|: - 1. Set |elementWithLocalAttributes| to - |configuration|["{{SanitizerConfig/elements}}"][|elementName|]. 1. [=list/iterate|For each=] |attribute| in |child|'s [=Element/attribute list=]: - 1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s - [=Attr/local name=] and [=Attr/namespace=]. - - 1. If |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/removeAttributes}}"] [=map/with default=] « » - [=SanitizerConfig/contains=] |attrName|: - 1. [=/remove an attribute|Remove=] |attribute|. - 1. Otherwise, if |configuration|["{{SanitizerConfig/attributes}}"] [=map/exists=]: - 1. If |configuration|["{{SanitizerConfig/attributes}}"] does not - [=SanitizerConfig/contain=] |attrName| and - |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] [=map/with default=] « » - does not [=SanitizerConfig/contain=] |attrName|, and if - "data-" is not a [=code unit prefix=] of |attribute|'s [=Attr/local name=] and - [=Attr/namespace=] is not `null` or - |configuration|["{{SanitizerConfig/dataAttributes}}"] is not true: - 1. [=/remove an attribute|Remove=] |attribute|. - 1. Otherwise: - 1. If |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] - [=map/exists=] and |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] does not [=SanitizerConfig/contain=] |attrName|: - 1. [=/remove an attribute|Remove=] |attribute|. - - 1. Otherwise, if |configuration|["{{SanitizerConfig/removeAttributes}}"] [=SanitizerConfig/contains=] |attrName|: - 1. [=/remove an attribute|Remove=] |attribute|. + 1. If [=is attribute allowed=] for |attribute| given |configuration|, + and |child| is [=/blocked=], + then [=/remove an attribute|remove=] |attribute|. 1. If |handleJavascriptNavigationUrls|: - 1. If «[|elementName|, |attrName|]» matches an entry in - the [=built-in navigating URL attributes list=], and if |attribute| - [=contains a javascript: URL=], then [=/remove an attribute|remove=] |attribute|. - 1. If |child|'s [=Element/namespace=] [=string/is=] the - [=MathML Namespace=] and |attr|'s [=Attr/local name=] [=string/is=] - "`href`" and |attr|'s [=Attr/namespace=] is `null` or the - [=XLink namespace=] and |attr| [=contains a javascript: URL=], - then [=/remove an attribute|remove=] |attribute|. - 1. If the [=built-in animating URL attributes list=] - [=SanitizerConfig/contains=] - «[|elementName|, |attrName|]» and |attr|'s - [=get an attribute value|value=] [=string/is=] "`href`" or - "`xlink:href`", then [=/remove an attribute|remove=] |attribute|. + 1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s + [=Attr/local name=] and [=Attr/namespace=]. + 1. If «[|elementName|, |attrName|]» matches an entry in + the [=built-in navigating URL attributes list=], and if |attribute| + [=contains a javascript: URL=], then [=/remove an attribute|remove=] + |attribute|. + 1. If |child|'s [=Element/namespace=] [=string/is=] the + [=MathML Namespace=] and |attr|'s [=Attr/local name=] [=string/is=] + "`href`" and |attr|'s [=Attr/namespace=] is `null` or the + [=XLink namespace=] and |attr| [=contains a javascript: URL=], + then [=/remove an attribute|remove=] |attribute|. + 1. If the [=built-in animating URL attributes list=] + [=SanitizerConfig/contains=] + «[|elementName|, |attrName|]» and |attr|'s + [=get an attribute value|value=] [=string/is=] "`href`" or + "`xlink:href`", then [=/remove an attribute|remove=] |attribute|. 1. Call [=sanitize core=] on |child| with |configuration| and |handleJavascriptNavigationUrls|. +
+To determine is attribute allowed for an |attribute|, +given a {{SanitizerConfig}} |configuration|, an {{Element}} |current element|: + +1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s + [=Attr/local name=] and [=Attr/namespace=]. +1. Let |elementName| be a {{SanitizerElementNamespace}} with |current element|'s + [=Element/local name=] and [=Element/namespace=]. +1. Let |elementWithLocalAttributes| be an empty [=ordered map=]. +1. If |configuration|["{{SanitizerConfig/elements}}"] [=map/exists=] and + |configuration|["{{SanitizerConfig/elements}}"] [=SanitizerConfig/contains=] + |elementName|: + 1. Set |elementWithLocalAttributes| to the |item| in + |configuration|["{{SanitizerConfig/elements}}"] where + |elementName|["{{SanitizerElementNamespace/name}}"] [=string/is=] + |item|["{{SanitizerElementNamespace/name}}"] and + |elementName|["{{SanitizerElementNamespace/namespace}}"] [=string/is=] + |item|["{{SanitizerElementNamespace/namespace}}"]. +1. If |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/removeAttributes}}"] [=map/with default=] « » + [=SanitizerConfig/contains=] |attrName|: + 1. Return [=/blocked=]. +1. If |configuration|["{{SanitizerConfig/attributes}}"] [=map/exists=]: + 1. Let the [=boolean=] |globallyAllowed| be whether + |configuration|["{{SanitizerConfig/attributes}}"] + [=SanitizerConfig/contains=] |attrName|. + 1. Let the [=boolean=] |locallyAllowed| be whether + |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] [=map/with default=] « » + [=SanitizerConfig/contains=] |attrName|. + 1. Let the [=boolean=] |isDataAttributeAllowed| be whether both, + "data-" is a [=code unit prefix=] of |attribute|'s + [=Attr/local name=] and [=Attr/namespace=] is `null`, and + |configuration|["{{SanitizerConfig/dataAttributes}}"] is true. + 1. If neither |globallyAllowed| nor |locallyAllowed| nor + |isDataAttributeAllowed|, return [=/blocked=]. +1. Otherwise: + 1. If |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] + [=map/exists=] and |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] does not [=SanitizerConfig/contain=] |attrName|: + 1. Return [=/blocked=]. +1. Return [=/allowed=]. + +
+ +
Note: Current browsers support `javascript:` URLs only when navigating. Since navigation itself is not an XSS threat we handle From 7b3f161648f66de6904f5497b8203c64f2ba476d Mon Sep 17 00:00:00 2001 From: Daniel Vogelheim Date: Wed, 13 May 2026 11:39:50 +0200 Subject: [PATCH 2/4] Review feedback: Call 'is attribute allowed' with canonicalized dicts, rather then DOM nodes. --- index.bs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/index.bs b/index.bs index f13c522..440a198 100644 --- a/index.bs +++ b/index.bs @@ -1033,13 +1033,12 @@ beginning with |node|. It consistes of these steps: then call [=sanitize core=] on |child|'s [=Element/shadow root=] with |configuration| and |handleJavascriptNavigationUrls|. 1. [=list/iterate|For each=] |attribute| in |child|'s [=Element/attribute list=]: - 1. If [=is attribute allowed=] for |attribute| given |configuration|, - and |child| is [=/blocked=], + 1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s + [=Attr/local name=] and [=Attr/namespace=]. + 1. If [=is attribute allowed=] for |attrName| given |configuration|, + and |elementName| is [=/blocked=], then [=/remove an attribute|remove=] |attribute|. - 1. If |handleJavascriptNavigationUrls|: - 1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s - [=Attr/local name=] and [=Attr/namespace=]. 1. If «[|elementName|, |attrName|]» matches an entry in the [=built-in navigating URL attributes list=], and if |attribute| [=contains a javascript: URL=], then [=/remove an attribute|remove=] @@ -1059,13 +1058,11 @@ beginning with |node|. It consistes of these steps:
-To determine is attribute allowed for an |attribute|, -given a {{SanitizerConfig}} |configuration|, an {{Element}} |current element|: +To determine is attribute allowed for a +{{SanitizerAttributeNamespace}} |attrName|, +given a {{SanitizerConfig}} |configuration|, +and an {{SanitizerElementNamespace}} |elementName|: -1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s - [=Attr/local name=] and [=Attr/namespace=]. -1. Let |elementName| be a {{SanitizerElementNamespace}} with |current element|'s - [=Element/local name=] and [=Element/namespace=]. 1. Let |elementWithLocalAttributes| be an empty [=ordered map=]. 1. If |configuration|["{{SanitizerConfig/elements}}"] [=map/exists=] and |configuration|["{{SanitizerConfig/elements}}"] [=SanitizerConfig/contains=] @@ -1087,8 +1084,9 @@ given a {{SanitizerConfig}} |configuration|, an {{Element}} |current element|: |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] [=map/with default=] « » [=SanitizerConfig/contains=] |attrName|. 1. Let the [=boolean=] |isDataAttributeAllowed| be whether both, - "data-" is a [=code unit prefix=] of |attribute|'s - [=Attr/local name=] and [=Attr/namespace=] is `null`, and + "data-" is a [=code unit prefix=] of + |attrName|[{{SanitizerAttributeNamespace/name}}] and + |attrName|[{{SanitizerAttributeNamespace/namespace}}] is `null`, and |configuration|["{{SanitizerConfig/dataAttributes}}"] is true. 1. If neither |globallyAllowed| nor |locallyAllowed| nor |isDataAttributeAllowed|, return [=/blocked=]. From 4930a4bc3e64d1ea1c125d9b276f0490f4c98a33 Mon Sep 17 00:00:00 2001 From: Daniel Vogelheim Date: Wed, 13 May 2026 19:12:29 +0200 Subject: [PATCH 3/4] Review feedback: Restore removeAttributes condition. --- index.bs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/index.bs b/index.bs index 440a198..dc9c6c1 100644 --- a/index.bs +++ b/index.bs @@ -1094,6 +1094,9 @@ and an {{SanitizerElementNamespace}} |elementName|: 1. If |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] [=map/exists=] and |elementWithLocalAttributes|["{{SanitizerElementNamespaceWithAttributes/attributes}}"] does not [=SanitizerConfig/contain=] |attrName|: 1. Return [=/blocked=]. + 1. Otherwise, if |configuration|["{{SanitizerConfig/removeAttributes}}"] + [=SanitizerConfig/contains=] |attrName|: + 1. Return [=/blocked=]. 1. Return [=/allowed=].
From 9d82a3636aee6530743eb25372328e3a300e8187 Mon Sep 17 00:00:00 2001 From: Tom Schuster Date: Thu, 16 Apr 2026 16:40:29 +0200 Subject: [PATCH 4/4] Remove custom element state when is attribute is blocked --- index.bs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/index.bs b/index.bs index dc9c6c1..9c51e69 100644 --- a/index.bs +++ b/index.bs @@ -1032,6 +1032,13 @@ beginning with |node|. It consistes of these steps: 1. If |child| is a [=shadow host=], then call [=sanitize core=] on |child|'s [=Element/shadow root=] with |configuration| and |handleJavascriptNavigationUrls|. + 1. If |child|'s [=is value=] is not `null`: + 1. Let |isAttrName| be «[ "`name`" → "`is`", "`namespace`" → null ]». + 1. If [=is attribute allowed=] for |isAttrName| given |configuration|, + and |elementName| is [=/blocked=]: + 1. Set |child|'s [=custom element state=] to "undefined". + 1. Set |child|'s [=custom element definition=] to `null`. + 1. Set |child|'s [=is value=] to `null`. 1. [=list/iterate|For each=] |attribute| in |child|'s [=Element/attribute list=]: 1. Let |attrName| be a {{SanitizerAttributeNamespace}} with |attribute|'s [=Attr/local name=] and [=Attr/namespace=].