tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 1e9a616c6b0c9c2902edf6b5c0ce755de68f639c
parent acb0b7a9f7f7ff7a82a71bbffac5a7086a2f0a4a
Author: Tom Schuster <tschuster@mozilla.com>
Date:   Mon, 20 Oct 2025 10:19:28 +0000

Bug 1994959 - Sanitizer: Don't allow local attributes+removeAttributes together with global removeAttributes. r=smaug

https://github.com/WICG/sanitizer-api/pull/311

Differential Revision: https://phabricator.services.mozilla.com/D269013

Diffstat:
Mdom/security/sanitizer/Sanitizer.cpp | 147+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtesting/web-platform/meta/sanitizer-api/sethtml-tree-construction.tentative.html.ini | 3---
Mtesting/web-platform/tests/sanitizer-api/sanitizer-config.tentative.html | 11+++++++++++
Mtesting/web-platform/tests/sanitizer-api/sanitizer-modifiers.tentative.html | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mtesting/web-platform/tests/sanitizer-api/support/util.js | 8++++++++
5 files changed, 191 insertions(+), 64 deletions(-)

diff --git a/dom/security/sanitizer/Sanitizer.cpp b/dom/security/sanitizer/Sanitizer.cpp @@ -536,6 +536,10 @@ void Sanitizer::IsValid(ErrorResult& aRv) { // Step 4. None of config[elements], config[removeElements], // config[replaceWithChildrenElements], config[attributes], or // config[removeAttributes], if they exist, has duplicates. + // + // NOTE: The Sanitizer::CanonicalizeConfiguration method would have already + // thrown an error for duplicate elements/attributes by this point. The map + // and sets can't have duplicates by definition. // Step 5. If both config[elements] and config[replaceWithChildrenElements] // exist, then the intersection of config[elements] and @@ -577,6 +581,11 @@ void Sanitizer::IsValid(ErrorResult& aRv) { // Step 7.1.1.1. Neither element[attributes] or // element[removeAttributes], if they exist, has duplicates. + // + // NOTE: The Sanitizer::CanonicalizeConfiguration (specifically + // CanonicalizeElementAttributes) method would have already thrown an + // error for duplicate attributes by this point. The attribute sets + // can't have duplicates by definition. // Step 7.1.1.2. The intersection of config[attributes] and // element[attributes] with default « [] » is empty. @@ -657,7 +666,24 @@ void Sanitizer::IsValid(ErrorResult& aRv) { for (const auto& entry : *mElements) { const CanonicalElementAttributes& elemAttributes = entry.GetData(); - // Step 8.1.1. The intersection of config[removeAttributes] and + // Step 8.1.1. Not both element[attributes] and + // element[removeAttributes] exist. + if (elemAttributes.mAttributes && elemAttributes.mRemoveAttributes) { + return aRv.ThrowTypeError( + nsFmtCString(FMT_STRING("Element {} can't have both 'attributes' " + "and 'removeAttributes'."), + entry.GetKey())); + } + + // Step 8.1.2. Neither element[attributes] nor + // element[removeAttributes], if they exist, has duplicates. + // + // NOTE: The Sanitizer::CanonicalizeConfiguration (specifically + // CanonicalizeElementAttributes) method would have already thrown an + // error for duplicate attributes by this point. The attribute sets + // can't have duplicates by definition. + + // Step 8.1.3. The intersection of config[removeAttributes] and // element[attributes] with default « [] » is empty. if (elemAttributes.mAttributes) { for (const CanonicalName& name : *elemAttributes.mAttributes) { @@ -672,7 +698,7 @@ void Sanitizer::IsValid(ErrorResult& aRv) { } } - // Step 8.1.2. The intersection of config[removeAttributes] and + // Step 8.1.4. The intersection of config[removeAttributes] and // element[removeAttributes] with default « [] » is empty. if (elemAttributes.mRemoveAttributes) { for (const CanonicalName& name : *elemAttributes.mRemoveAttributes) { @@ -885,78 +911,111 @@ bool Sanitizer::AllowElement( // Step 2.2. Comment: We need to make sure the per-element attributes do // not overlap with global attributes. - // Step 2.3. If element["attributes"] exists: - if (elementAttributes.mAttributes) { - CanonicalNameSet attributes; - for (const CanonicalName& attr : *elementAttributes.mAttributes) { - // Step 2.3.1. Set element["attributes"] to remove duplicates from - // element["attributes"]. - MOZ_ASSERT(!attributes.Contains(attr)); - - // Step 2.3.2. If configuration["attributes"] exists: - if (mAttributes) { - // Step 2.3.2.1. Set element["attributes"] to the difference of + // Step 2.3. If configuration["attributes"] exists: + if (mAttributes) { + // Step 2.3.1. If element["attributes"] exists: + if (elementAttributes.mAttributes) { + CanonicalNameSet attributes; + for (const CanonicalName& attr : *elementAttributes.mAttributes) { + // Step 2.3.1.1. Set element["attributes"] to remove duplicates from + // element["attributes"]. + MOZ_ASSERT(!attributes.Contains(attr)); + + // Step 2.3.1.2. Set element["attributes"] to the difference of // element["attributes"] and configuration["attributes"]. if (mAttributes->Contains(attr)) { continue; } + // Step 2.3.1.3. If configuration["dataAttributes"] is true: MOZ_ASSERT(mDataAttributes.isSome(), "mDataAttributes exists iff mAttributes"); - - // Step 2.3.2.2. If configuration["dataAttributes"] is true: if (*mDataAttributes) { - // Step 2.3.2.2.1. Remove all items item from element["attributes"] + // Step 2.3.1.3.1 Remove all items item from element["attributes"] // where item is a custom data attribute. if (attr.IsDataAttribute()) { continue; } } + + attributes.Insert(attr.Clone()); } + elementAttributes.mAttributes = Some(std::move(attributes)); + } - // Step 2.3.3. If configuration["removeAttributes"] exists: - if (mRemoveAttributes) { - // Step 2.3.3.1. Set element["attributes"] to the difference of - // element["attributes"] and configuration["removeAttributes"]. - if (mRemoveAttributes->Contains(attr)) { + // Step 2.3.2. If element["removeAttributes"] exists: + if (elementAttributes.mRemoveAttributes) { + CanonicalNameSet removeAttributes; + for (const CanonicalName& attr : *elementAttributes.mRemoveAttributes) { + // Step 2.3.2.1. Set element["removeAttributes"] to remove duplicates + // from element["removeAttributes"]. + // + // NOTE: CanonicalizeElementAttributes removed all duplicates for us. + MOZ_ASSERT(!removeAttributes.Contains(attr)); + + // Step 2.3.2.2. Set element["removeAttributes"] to the intersection + // of element["removeAttributes"] and configuration["attributes"]. + if (!mAttributes->Contains(attr)) { continue; } - } - attributes.Insert(attr.Clone()); + removeAttributes.Insert(attr.Clone()); + } + elementAttributes.mRemoveAttributes = Some(std::move(removeAttributes)); } - elementAttributes.mAttributes = Some(std::move(attributes)); - } + } else { + // Step 2.4. Otherwise: - // Step 2.4. If element["removeAttributes"] exists: - if (elementAttributes.mRemoveAttributes) { - CanonicalNameSet removeAttributes; - for (const CanonicalName& attr : *elementAttributes.mRemoveAttributes) { - // Step 2.4.1. Set element["removeAttributes"] to remove duplicates from - // element["removeAttributes"]. - MOZ_ASSERT(!removeAttributes.Contains(attr)); - - // Step 2.4.2. If configuration["attributes"] exists: - if (mAttributes) { - // Step 2.4.2.1. Set element["removeAttributes"] to the intersection - // of element["removeAttributes"] and configuration["attributes"]. - if (!mAttributes->Contains(attr)) { + // Step 2.4.1. If element["attributes"] exists: + if (elementAttributes.mAttributes) { + CanonicalNameSet attributes; + for (const CanonicalName& attr : *elementAttributes.mAttributes) { + // Step 2.4.1.1. Set element["attributes"] to remove duplicates from + // element["attributes"]. + // + // NOTE: CanonicalizeElementAttributes removed all duplicates for us. + MOZ_ASSERT(!attributes.Contains(attr)); + + // Step 2.4.1.2. Set element["attributes"] to the difference of + // element["attributes"] and element["removeAttributes"] with default + // « ». + if (elementAttributes.mRemoveAttributes && + elementAttributes.mRemoveAttributes->Contains(attr)) { continue; } + + // Step 2.4.1.4. Set element["attributes"] to the difference of + // element["attributes"] and configuration["removeAttributes"]. + if (mRemoveAttributes->Contains(attr)) { + continue; + } + + attributes.Insert(attr.Clone()); } + elementAttributes.mAttributes = Some(std::move(attributes)); + + // Step 2.4.1.3. Remove element["removeAttributes"]. + elementAttributes.mRemoveAttributes = Nothing(); + } + + // Step 2.4.2. If element["removeAttributes"] exists: + if (elementAttributes.mRemoveAttributes) { + CanonicalNameSet removeAttributes; + for (const CanonicalName& attr : *elementAttributes.mRemoveAttributes) { + // Step 2.4.2.1. Set element["removeAttributes"] to remove duplicates + // from element["removeAttributes"]. + MOZ_ASSERT(!removeAttributes.Contains(attr)); - // Step 2.4.3. If configuration["removeAttributes"] exists: - if (mRemoveAttributes) { - // Step 2.4.3.1. Set element["removeAttributes"] to the difference of + // Step 2.4.2.2. Set element["removeAttributes"] to the difference of // element["removeAttributes"] and configuration["removeAttributes"]. if (mRemoveAttributes->Contains(attr)) { continue; } - } - removeAttributes.Insert(attr.Clone()); + removeAttributes.Insert(attr.Clone()); + } + elementAttributes.mRemoveAttributes = Some(std::move(removeAttributes)); } - elementAttributes.mRemoveAttributes = Some(std::move(removeAttributes)); } // Step 2.5. If configuration["elements"] does not contain element: diff --git a/testing/web-platform/meta/sanitizer-api/sethtml-tree-construction.tentative.html.ini b/testing/web-platform/meta/sanitizer-api/sethtml-tree-construction.tentative.html.ini @@ -1,6 +1,3 @@ [sethtml-tree-construction.tentative.html] - [Testcase #28, "<div id='div' title='div'>DIV</div>", config: "{ "elements": [{ "name": "div", "attributes": ["id"\], "removeAttributes": ["id"\] }\]}".] - expected: FAIL - [Testcase #71, "<table><div><td>", config: "{ "replaceWithChildrenElements": ["table"\] }".] expected: FAIL diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-config.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-config.tentative.html @@ -348,6 +348,17 @@ test(() => { }, "Global attribute allow-list with a data attribute must not co-exist with config[dataAttributes] set to true."); // If config[removeAttributes] exists: + +// Not both element[attributes] and element[removeAttributes] exist. +test(() => { + assert_throws_js(TypeError, () => { + new Sanitizer({ + removeAttributes: [], + elements: [{ name: "div", attributes: [], removeAttributes: [] }], + }); + }); +}, "element[attributes] and element[removeAttributes] should not both exist with config[removeAttributes]."); + // The intersection of config[removeAttributes] and element[attributes] with default « » is empty. test(() => { assert_throws_js(TypeError, () => { diff --git a/testing/web-platform/tests/sanitizer-api/sanitizer-modifiers.tentative.html b/testing/web-platform/tests/sanitizer-api/sanitizer-modifiers.tentative.html @@ -70,7 +70,7 @@ test(() => { }, "sanitizer.allowAttribute() with global attributes and elements"); test(() => { - const ELEMENTS = [{ name: "div", attributes: ["href"], removeAttributes: ["title"] }]; + const ELEMENTS = [{ name: "div", attributes: ["href"] }]; let s = new Sanitizer({ removeAttributes: ["id"], @@ -100,7 +100,41 @@ test(() => { removeAttributes: [], elements: ELEMENTS, }); -}, "sanitizer.allowAttribute() with global removeAttributes and elements"); +}, "sanitizer.allowAttribute() with global removeAttributes and element's attributes"); + +test(() => { + const ELEMENTS = [{ name: "div", removeAttributes: ["href"] }]; + + let s = new Sanitizer({ + removeAttributes: ["id"], + elements: ELEMENTS, + }); + + assert_false(s.allowAttribute("class")); + assert_config(s.get(), { + removeAttributes: ["id"], + elements: ELEMENTS, + }); + + assert_true(s.allowAttribute("id")); + assert_config(s.get(), { + removeAttributes: [], + elements: ELEMENTS, + }); + + assert_false(s.allowAttribute("href")); + assert_config(s.get(), { + removeAttributes: [], + elements: ELEMENTS, + }); + + assert_false(s.allowAttribute("title")); + assert_config(s.get(), { + removeAttributes: [], + elements: ELEMENTS, + }); +}, "sanitizer.allowAttribute() with global removeAttributes and element's removeAttributes"); + test(() => { let s = new Sanitizer({ @@ -166,33 +200,52 @@ test(() => { test(() => { let s = new Sanitizer({ removeAttributes: ["title"], - elements: [{ name: "div", attributes: ["class"], removeAttributes: ["id"] }], + elements: [{ name: "div", attributes: ["class"] }], }); assert_false(s.removeAttribute({ name: "title", namespace: null })); assert_config(s.get(), { removeAttributes: ["title"], - elements: [{ name: "div", attributes: ["class"], removeAttributes: ["id"] }], + elements: [{ name: "div", attributes: ["class"] }], }); assert_true(s.removeAttribute("dir")); assert_config(s.get(), { removeAttributes: ["dir", "title"], - elements: [{ name: "div", attributes: ["class"], removeAttributes: ["id"] }], + elements: [{ name: "div", attributes: ["class"] }], }); - assert_true(s.removeAttribute("id")); + assert_true(s.removeAttribute("class")); assert_config(s.get(), { - removeAttributes: ["dir", "id", "title"], - elements: [{ name: "div", attributes: ["class"], removeAttributes: [] }], + removeAttributes: ["class", "dir", "title"], + elements: [{ name: "div", attributes: [] }], }); +}, "sanitizer.removeAttribute() with global removeAttributes and element's attributes"); - assert_true(s.removeAttribute("class")); +test(() => { + let s = new Sanitizer({ + removeAttributes: ["title"], + elements: [{ name: "div", removeAttributes: ["id"] }], + }); + + assert_false(s.removeAttribute({ name: "title", namespace: null })); + assert_config(s.get(), { + removeAttributes: ["title"], + elements: [{ name: "div", removeAttributes: ["id"] }], + }); + + assert_true(s.removeAttribute("dir")); + assert_config(s.get(), { + removeAttributes: ["dir", "title"], + elements: [{ name: "div", removeAttributes: ["id"] }], + }); + + assert_true(s.removeAttribute("id")); assert_config(s.get(), { - removeAttributes: ["class", "dir", "id", "title"], - elements: [{ name: "div", attributes: [], removeAttributes: [] }], + removeAttributes: ["dir", "id", "title"], + elements: [{ name: "div", removeAttributes: [] }], }); -}, "sanitizer.removeAttribute() with global removeAttributes and elements"); +}, "sanitizer.removeAttribute() with global removeAttributes and element's removeAttributes"); test(() => { let s = new Sanitizer({ @@ -473,15 +526,14 @@ test(() => { elements: [{name: "p", attributes: undefined, removeAttributes: ["class"]}] }); - // XXX https://github.com/WICG/sanitizer-api/issues/305 - assert_true(s.allowElement({ name: "p", attributes: ["id", "dir"], removeAttributes: ["id", "lang"] })) + assert_true(s.allowElement({ name: "p", attributes: ["id", "dir", "lang"], removeAttributes: ["id", "lang"] })) assert_config(s.get(), { - elements: [{name: "p", attributes: ["dir"], removeAttributes: ["lang"]}] + elements: [{name: "p", attributes: ["dir"], removeAttributes: undefined}] }); // Duplicate - assert_false(s.allowElement({ name: "p", attributes: ["id", "dir"], removeAttributes: ["id", "lang"] })) + assert_false(s.allowElement({ name: "p", attributes: ["id", "dir", "lang"], removeAttributes: ["id", "lang"] })) assert_config(s.get(), { - elements: [{name: "p", attributes: ["dir"], removeAttributes: ["lang"]}] + elements: [{name: "p", attributes: ["dir"], removeAttributes: undefined}] }); assert_true(s.allowElement({name: "p"})); diff --git a/testing/web-platform/tests/sanitizer-api/support/util.js b/testing/web-platform/tests/sanitizer-api/support/util.js @@ -51,6 +51,14 @@ function assert_config_is_valid(config) { // If config[attributes] exists: if (config.attributes) { } else { + if (config.elements) { + for (let element of config.elements) { + // Not both element[attributes] and element[removeAttributes] exist. + assert_false("attributes" in element && "removeAttributes" in element, + `Element ${element.name} can't have both 'attributes' and 'removeAttributes'`); + } + } + // config[dataAttributes] does not exist. assert_false("dataAttributes" in config, "dataAttributes does not exist"); }