Bug 1782910 - Sanitizer: Implement the element matches an element name. r=emilio

This is still missing the part that normalizes lower-cased svg/mathml names.

Differential Revision: https://phabricator.services.mozilla.com/D154654
This commit is contained in:
Tom Schuster
2022-08-31 13:35:31 +00:00
parent ca98419193
commit 1c8817cc50
4 changed files with 153 additions and 143 deletions

View File

@@ -1590,8 +1590,8 @@ bool nsTreeSanitizer::MustFlattenForSanitizerAPI(int32_t aNamespace,
// Step 6. If element matches any name in config["blockElements"]: Return
// block.
// TODO(bug 1782910): "matches" is not really contains!
if (mBlockElements && mBlockElements->Contains(aLocal)) {
if (mBlockElements &&
MatchesElementName(*mBlockElements, aNamespace, aLocal)) {
return true;
}
@@ -1601,19 +1601,20 @@ bool nsTreeSanitizer::MustFlattenForSanitizerAPI(int32_t aNamespace,
if (mAllowElements) {
// Step 9. If element does not match any name in allow list:
// Return block.
// TODO(bug 1782910): matches
if (!mAllowElements->Contains(aLocal)) {
if (!MatchesElementName(*mAllowElements, aNamespace, aLocal)) {
return true;
}
} else {
// Step 8.2. Otherwise: Set allow list to the default configuration's
// element allow list.
// Step 9. If element does not match any name in allow list:
// Return block.
// TODO(bug 1782910): matches
if (!sDefaultConfigurationElementAllowlist->Contains(aLocal)) {
// The default configuration only contains HTML elements, so we can
// reject everything else.
if (aNamespace != kNameSpaceID_XHTML ||
!sDefaultConfigurationElementAllowlist->Contains(aLocal)) {
return true;
}
}
@@ -1763,8 +1764,7 @@ bool nsTreeSanitizer::MustPruneForSanitizerAPI(int32_t aNamespace,
}
// Step 5. If element matches any name in config["dropElements"]: Return drop.
// TODO(bug 1782910): "matches" is not really contains!
if (mDropElements && mDropElements->Contains(aLocal)) {
if (mDropElements && MatchesElementName(*mDropElements, aNamespace, aLocal)) {
return true;
}
@@ -1964,34 +1964,41 @@ void nsTreeSanitizer::SanitizeAttributes(mozilla::dom::Element* aElement,
}
}
// https://wicg.github.io/sanitizer-api/#element-matches-an-element-name
bool nsTreeSanitizer::MatchesElementName(ElementNameSet& aNames,
int32_t aNamespace,
nsAtom* aLocalName) {
return aNames.Contains(ElementName(aNamespace, aLocalName));
}
// https://wicg.github.io/sanitizer-api/#attribute-match-list
bool nsTreeSanitizer::MatchesAttributeMatchList(
ElementToAttributeSetTable& aMatchList, Element& aElement,
int32_t aAttrNamespace, nsAtom* aAttrLocalName) {
// Step 1. If attributes local name does not match the attribute match list
// lists key and if the key is not "*": Return false.
DynamicAtomsTable* elements;
ElementNameSet* names;
if (auto lookup = aMatchList.Lookup(aAttrLocalName)) {
elements = lookup->get();
names = lookup->get();
} else if (auto lookup = aMatchList.Lookup(nsGkAtoms::_asterisk)) {
elements = lookup->get();
names = lookup->get();
} else {
return false;
}
// Step 2. Let element be the attributes Element.
// Step 3. Let element name be elements local name.
nsAtom* elemName = aElement.NodeInfo()->NameAtom();
// Step 4. If element is a in either the SVG or MathML namespaces (i.e., its
// a foreign element), then prefix element name with the appropriate namespace
// designator plus a whitespace character.
// TODO(bug 1784040) Namespace handling.
int32_t namespaceID = aElement.NodeInfo()->NamespaceID();
RefPtr<nsAtom> nameAtom = aElement.NodeInfo()->NameAtom();
ElementName elemName(namespaceID, nameAtom);
// Step 5. If lists value does not contain element name and value is not
// ["*"]: Return false.
if (!elements->Contains(elemName) &&
!elements->Contains(nsGkAtoms::_asterisk)) {
if (!names->Contains(elemName) &&
!names->Contains(ElementName(kNameSpaceID_XHTML, nsGkAtoms::_asterisk))) {
return false;
}
@@ -2452,6 +2459,50 @@ void nsTreeSanitizer::ReleaseStatics() {
NS_IF_RELEASE(sNullPrincipal);
}
UniquePtr<nsTreeSanitizer::ElementNameSet> nsTreeSanitizer::ConvertElementNames(
const Sequence<nsString>& aNames) {
auto names = MakeUnique<ElementNameSet>(aNames.Length());
// https://wicg.github.io/sanitizer-api/#normalize-element-name
for (const nsString& name : aNames) {
// Step 1. Let tokens be the result of strictly splitting name on the
// delimiter ":" (U+003A).
int32_t index = name.FindChar(':');
// Step 2. If tokens size is 1, then return tokens[0].
if (index == kNotFound) {
RefPtr<nsAtom> nameAtom = NS_AtomizeMainThread(name);
ElementName elemName(kNameSpaceID_XHTML, std::move(nameAtom));
names->Insert(elemName);
continue;
}
// Step 3. If tokens size is 2 and tokens[0] is either "svg" or "math",
// then:
if (name.FindChar(':', index + 1) == kNotFound) {
auto prefix = Substring(name, 0, index);
// Step 3.1. Adjust tokens[1] as described in the "any other start tag"
// branch of the rules for parsing tokens in foreign content subchapter in
// the HTML parsing spec. Step 3.2 Return the concatenation of the list
// «|tokens|[0],":" (U+003A),|tokens|[1]».
// TODO
RefPtr<nsAtom> nameAtom =
NS_AtomizeMainThread(Substring(name, index + 1));
if (prefix.EqualsLiteral("svg")) {
ElementName elemName(kNameSpaceID_SVG, std::move(nameAtom));
names->Insert(elemName);
} else if (prefix.EqualsLiteral("math")) {
ElementName elemName(kNameSpaceID_MathML, std::move(nameAtom));
names->Insert(elemName);
}
}
// Step 4. Return null.
// Nothing is inserted and name is skipped.
}
return names;
}
void nsTreeSanitizer::WithWebSanitizerOptions(
nsIGlobalObject* aGlobal, const mozilla::dom::SanitizerConfig& aOptions) {
if (StaticPrefs::dom_security_sanitizer_logging()) {
@@ -2478,46 +2529,25 @@ void nsTreeSanitizer::WithWebSanitizerOptions(
}
if (aOptions.mAllowElements.WasPassed()) {
const Sequence<nsString>& allowedElements = aOptions.mAllowElements.Value();
mAllowElements = MakeUnique<DynamicAtomsTable>(allowedElements.Length());
for (const nsString& elem : allowedElements) {
RefPtr<nsAtom> elAsAtom = NS_AtomizeMainThread(elem);
mAllowElements->Insert(elAsAtom);
}
mAllowElements = ConvertElementNames(aOptions.mAllowElements.Value());
}
if (aOptions.mBlockElements.WasPassed()) {
const Sequence<nsString>& blockedElements = aOptions.mBlockElements.Value();
mBlockElements = MakeUnique<DynamicAtomsTable>(blockedElements.Length());
for (const nsString& elem : blockedElements) {
RefPtr<nsAtom> elAsAtom = NS_AtomizeMainThread(elem);
mBlockElements->Insert(elAsAtom);
}
mBlockElements = ConvertElementNames(aOptions.mBlockElements.Value());
}
if (aOptions.mDropElements.WasPassed()) {
const Sequence<nsString>& dropElements = aOptions.mDropElements.Value();
mDropElements = MakeUnique<DynamicAtomsTable>(dropElements.Length());
for (const nsString& elem : dropElements) {
RefPtr<nsAtom> elAsAtom = NS_AtomizeMainThread(elem);
mDropElements->Insert(elAsAtom);
}
mDropElements = ConvertElementNames(aOptions.mDropElements.Value());
}
if (aOptions.mAllowAttributes.WasPassed()) {
const Record<nsString, Sequence<nsString>>& allowedAttributes =
aOptions.mAllowAttributes.Value();
mAllowedAttributes = MakeUnique<ElementToAttributeSetTable>();
nsAutoString name;
for (const auto& entries : allowedAttributes.Entries()) {
UniquePtr<DynamicAtomsTable> elems =
MakeUnique<DynamicAtomsTable>(allowedAttributes.Entries().Length());
for (const auto& elem : entries.mValue) {
RefPtr<nsAtom> elAsAtom = NS_AtomizeMainThread(elem);
elems->Insert(elAsAtom);
}
RefPtr<nsAtom> attrAtom = NS_AtomizeMainThread(entries.mKey);
mAllowedAttributes->InsertOrUpdate(attrAtom, std::move(elems));
for (const auto& entry : allowedAttributes.Entries()) {
RefPtr<nsAtom> attrAtom = NS_AtomizeMainThread(entry.mKey);
UniquePtr<ElementNameSet> elements = ConvertElementNames(entry.mValue);
mAllowedAttributes->InsertOrUpdate(attrAtom, std::move(elements));
}
}
@@ -2525,16 +2555,10 @@ void nsTreeSanitizer::WithWebSanitizerOptions(
const Record<nsString, Sequence<nsString>>& droppedAttributes =
aOptions.mDropAttributes.Value();
mDroppedAttributes = MakeUnique<ElementToAttributeSetTable>();
nsAutoString name;
for (const auto& entries : droppedAttributes.Entries()) {
UniquePtr<DynamicAtomsTable> elems =
MakeUnique<DynamicAtomsTable>(droppedAttributes.Entries().Length());
for (const auto& elem : entries.mValue) {
RefPtr<nsAtom> elAsAtom = NS_AtomizeMainThread(elem);
elems->Insert(elAsAtom);
}
RefPtr<nsAtom> attrAtom = NS_AtomizeMainThread(entries.mKey);
mDroppedAttributes->InsertOrUpdate(attrAtom, std::move(elems));
for (const auto& entry : droppedAttributes.Entries()) {
RefPtr<nsAtom> attrAtom = NS_AtomizeMainThread(entry.mKey);
UniquePtr<ElementNameSet> elements = ConvertElementNames(entry.mValue);
mDroppedAttributes->InsertOrUpdate(attrAtom, std::move(elements));
}
}
}

View File

@@ -12,6 +12,7 @@
#include "nsTArray.h"
#include "nsTHashSet.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/dom/NameSpaceConstants.h"
#include "mozilla/dom/SanitizerBinding.h"
class nsIContent;
@@ -129,17 +130,44 @@ class nsTreeSanitizer {
return aAtom->IsStatic() && GetEntry(aAtom->AsStatic());
}
};
// Use this table for user-defined lists
class DynamicAtomsTable : public nsTHashSet<RefPtr<nsAtom>> {
public:
explicit DynamicAtomsTable(uint32_t aLength)
: nsTHashSet<RefPtr<nsAtom>>(aLength) {}
bool Contains(nsAtom* aAtom) { return GetEntry(aAtom); }
// The name of an element combined with its namespace.
class ElementName : public PLDHashEntryHdr {
public:
using KeyType = const ElementName&;
using KeyTypePointer = const ElementName*;
explicit ElementName(KeyTypePointer aKey)
: mNamespaceID(aKey->mNamespaceID), mLocalName(aKey->mLocalName) {}
ElementName(int32_t aNamespaceID, RefPtr<nsAtom> aLocalName)
: mNamespaceID(aNamespaceID), mLocalName(std::move(aLocalName)) {}
ElementName(ElementName&&) = default;
~ElementName() = default;
bool KeyEquals(KeyTypePointer aKey) const {
return mNamespaceID == aKey->mNamespaceID &&
mLocalName == aKey->mLocalName;
}
static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
static PLDHashNumber HashKey(KeyTypePointer aKey) {
if (!aKey) {
return 0;
}
return mozilla::HashGeneric(aKey->mNamespaceID, aKey->mLocalName.get());
}
enum { ALLOW_MEMMOVE = true };
private:
int32_t mNamespaceID = kNameSpaceID_None;
RefPtr<nsAtom> mLocalName;
};
using ElementNameSet = nsTHashSet<ElementName>;
using ElementToAttributeSetTable =
nsTHashMap<RefPtr<nsAtom>, mozilla::UniquePtr<DynamicAtomsTable>>;
nsTHashMap<RefPtr<nsAtom>, mozilla::UniquePtr<ElementNameSet>>;
void SanitizeChildren(nsINode* aRoot);
@@ -249,11 +277,16 @@ class nsTreeSanitizer {
*/
static void RemoveAllAttributesFromDescendants(mozilla::dom::Element*);
static bool MatchesElementName(ElementNameSet& aNames, int32_t aNamespace,
nsAtom* aLocalName);
static bool MatchesAttributeMatchList(ElementToAttributeSetTable& aMatchList,
mozilla::dom::Element& aElement,
int32_t aAttrNamespace,
nsAtom* aAttrLocalName);
static mozilla::UniquePtr<ElementNameSet> ConvertElementNames(
const mozilla::dom::Sequence<nsString>& aNames);
/**
* Log a Console Service message to indicate we removed something.
* If you pass an element and/or attribute, their information will
@@ -338,13 +371,13 @@ class nsTreeSanitizer {
bool mAllowUnknownMarkup = false;
// An allow-list of elements to keep.
mozilla::UniquePtr<DynamicAtomsTable> mAllowElements;
mozilla::UniquePtr<ElementNameSet> mAllowElements;
// A deny-list of elements to block. (aka flatten)
mozilla::UniquePtr<DynamicAtomsTable> mBlockElements;
mozilla::UniquePtr<ElementNameSet> mBlockElements;
// A deny-list of elements to drop. (aka prune)
mozilla::UniquePtr<DynamicAtomsTable> mDropElements;
mozilla::UniquePtr<ElementNameSet> mDropElements;
// An allow-list of attributes to keep.
mozilla::UniquePtr<ElementToAttributeSetTable> mAllowedAttributes;

View File

@@ -14,79 +14,22 @@
[Attribute names in config item: dropAttributes]
expected: FAIL
[Namespaced elements #0: allowElements: ["p"\]]
expected: FAIL
[Namespaced elements #1: allowElements: ["svg"\]]
expected: FAIL
[Namespaced elements #2: allowElements: ["svg:svg"\]]
expected: FAIL
[Namespaced elements #3: allowElements: ["math"\]]
expected: FAIL
[Namespaced elements #4: allowElements: ["svg:math"\]]
expected: FAIL
[Namespaced elements #5: allowElements: ["math:math"\]]
expected: FAIL
[Namespaced elements #6: allowElements: ["potato:math"\]]
expected: FAIL
[Namespaced elements #7: allowElements: ["potato:math"\]]
expected: FAIL
[Namespaced attributes #0: allowAttributes: {"style": ["*"\]}]
expected: FAIL
[Namespaced attributes #1: allowAttributes: {"href": ["*"\]}]
expected: FAIL
[Namespaced attributes #2: allowAttributes: {"xlink:href": ["*"\]}]
expected: FAIL
[Namespaced attributes #3: allowAttributes: {"potato:href": ["*"\]}]
expected: FAIL
[Namespaced attributes #4: allowAttributes: {"xlink:href": ["*"\]}]
expected: FAIL
[Namespaced attributes #5: allowAttributes: {"href": ["*"\]}]
expected: FAIL
[Mixed-case element names #0: "svg:feBlend"]
expected: FAIL
[Mixed-case element names #0: "svg:feblend"]
expected: FAIL
[Mixed-case element names #0: "SVG:FEBLEND"]
[Lower-case element names #0: "svg:feblend"]
expected: FAIL
[Mixed case element names #0: "svg:feBlend" is preserved in config.]
expected: FAIL
[Mixed-case element names #1: "svg:feColorMatrix"]
expected: FAIL
[Mixed-case element names #1: "svg:fecolormatrix"]
expected: FAIL
[Mixed-case element names #1: "SVG:FECOLORMATRIX"]
[Lower-case element names #1: "svg:fecolormatrix"]
expected: FAIL
[Mixed case element names #1: "svg:feColorMatrix" is preserved in config.]
expected: FAIL
[Mixed-case element names #2: "svg:textPath"]
expected: FAIL
[Mixed-case element names #2: "svg:textpath"]
expected: FAIL
[Mixed-case element names #2: "SVG:TEXTPATH"]
[Lower-case element names #2: "svg:textpath"]
expected: FAIL
[Mixed case element names #2: "svg:textPath" is preserved in config.]

View File

@@ -65,9 +65,12 @@
[ "potato:math", "<potato:math>Hello</potato:math>", "Hello" ],
].forEach(([elem, probe, expected], index) => {
test(t => {
const sanitizer = new Sanitizer({allowElements: [elem]});
assert_equals(sanitizer.sanitizeFor("template", probe).innerHTML,
expected ?? probe);
const sanitizer = new Sanitizer({allowElements: [elem],
// TODO(https://github.com/WICG/sanitizer-api/issues/167)
allowUnknownMarkup: true});
const template = document.createElement("template");
template.setHTML(probe, {sanitizer});
assert_equals(template.innerHTML, expected ?? probe);
}, `Namespaced elements #${index}: allowElements: ["${elem}"]`);
});
@@ -81,39 +84,46 @@
[ "href", "<p xlink:href='bla'></p>", "<p></p>" ],
].forEach(([attr, probe, expected], index) => {
test(t => {
const sanitizer = new Sanitizer({allowAttributes: {[attr]: ["*"]}});
assert_equals(sanitizer.sanitizeFor("template", probe).innerHTML,
expected ?? probe);
const sanitizer = new Sanitizer({allowAttributes: {[attr]: ["*"]},
// TODO(https://github.com/WICG/sanitizer-api/issues/167)
allowUnknownMarkup: true});
const template = document.createElement("template");
template.setHTML(probe, {sanitizer});
assert_equals(template.innerHTML, expected ?? probe);
}, `Namespaced attributes #${index}: allowAttributes: {"${attr}": ["*"]}`);
});
// Most element and attribute names are lower-cased, but "foreign content"
// like SVG and MathML have some mixed-cased names.
[
[ "svg:feBlend", "<feBlend></feBlend>" ],
[ "svg:feColorMatrix", "<feColorMatrix></feColorMatrix>" ],
[ "svg:textPath", "<textPath></textPath>" ],
[ "feBlend", "<feBlend></feBlend>" ],
[ "feColorMatrix", "<feColorMatrix></feColorMatrix>" ],
[ "textPath", "<textPath></textPath>" ],
].forEach(([elem, probe], index) => {
const sanitize = (elem, probe) => {
return new Sanitizer({allowElements: ["svg:svg", elem]}).
sanitizeFor("template", `<svg>${probe}</svg`).
content.firstElementChild.innerHTML;
const sanitizer = new Sanitizer({allowElements: ["svg:svg", "svg:" + elem],
// TODO(https://github.com/WICG/sanitizer-api/issues/167)
allowUnknownMarkup: true});
const template = document.createElement("template");
template.setHTML(`<svg>${probe}</svg>`, {sanitizer});
return template.content.firstElementChild.innerHTML;
};
test(t => {
assert_equals(sanitize(elem, probe), probe);
}, `Mixed-case element names #${index}: "${elem}"`);
}, `Mixed-case element names #${index}: "svg:${elem}"`);
test(t => {
assert_not_equals(sanitize(elem.toLowerCase(), probe), probe);
}, `Mixed-case element names #${index}: "${elem.toLowerCase()}"`);
// Lowercase element names should be normalized to mixed-case.
assert_equals(sanitize(elem.toLowerCase(), probe), probe);
}, `Lower-case element names #${index}: "svg:${elem.toLowerCase()}"`);
test(t => {
assert_not_equals(sanitize(elem.toUpperCase(), probe), probe);
}, `Mixed-case element names #${index}: "${elem.toUpperCase()}"`);
}, `Upper-case element names #${index}: "svg:${elem.toUpperCase()}"`);
test(t => {
const elems = [elem];
const elems = ["svg:" + elem];
assert_array_equals(
new Sanitizer({allowElements: elems}).getConfiguration().allowElements,
elems);
}, `Mixed case element names #${index}: "${elem}" is preserved in config.`);
}, `Mixed case element names #${index}: "svg:${elem}" is preserved in config.`);
});
</script>
</body>