diff --git a/browser/base/content/browser-media.js b/browser/base/content/browser-media.js index 5719f27d861c9..6343b3c0d87c0 100644 --- a/browser/base/content/browser-media.js +++ b/browser/base/content/browser-media.js @@ -34,11 +34,23 @@ var gEMEHandler = { } return true; }, - getLearnMoreLink(msgId) { - let text = gNavigatorBundle.getString("emeNotifications." + msgId + ".learnMoreLabel"); + getEMEDisabledFragment(msgId) { + let mainMessage = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.message"); + let [prefix, suffix] = mainMessage.split(/%(?:1\$)?S/).map(s => document.createTextNode(s)); + let text = gNavigatorBundle.getString("emeNotifications.drmContentDisabled.learnMoreLabel"); let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); - return ""; + let link = document.createElement("label"); + link.className = "text-link"; + link.setAttribute("href", baseURL + "drm-content"); + link.textContent = text; + + let fragment = document.createDocumentFragment(); + [prefix, link, suffix].forEach(n => fragment.appendChild(n)); + return fragment; + }, + getMessageWithBrandName(notificationId) { + let msgId = "emeNotifications." + notificationId + ".message"; + return gNavigatorBundle.getFormattedString(msgId, [this._brandShortName]); }, receiveMessage({target: browser, data: data}) { let parsedData; @@ -56,8 +68,8 @@ var gEMEHandler = { let notificationId; let buttonCallback; - let params = []; - switch (status) { + // Notification message can be either a string or a DOM fragment. + let notificationMessage; switch (status) { case "available": case "cdm-created": // Only show the chain icon for proprietary CDMs. Clearkey is not one. @@ -70,18 +82,18 @@ var gEMEHandler = { case "api-disabled": case "cdm-disabled": notificationId = "drmContentDisabled"; - buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem) - params = [this.getLearnMoreLink(notificationId)]; + buttonCallback = gEMEHandler.ensureEMEEnabled.bind(gEMEHandler, browser, keySystem); + notificationMessage = this.getEMEDisabledFragment(); break; case "cdm-insufficient-version": notificationId = "drmContentCDMInsufficientVersion"; - params = [this._brandShortName]; + notificationMessage = this.getMessageWithBrandName(notificationId); break; case "cdm-not-installed": notificationId = "drmContentCDMInstalling"; - params = [this._brandShortName]; + notificationMessage = this.getMessageWithBrandName(notificationId); break; case "cdm-not-supported": @@ -92,45 +104,28 @@ var gEMEHandler = { Cu.reportError(new Error("Unknown message ('" + status + "') dealing with EME key request: " + data)); return; } + + // Now actually create the notification - this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback); - }, - showNotificationBar(browser, notificationId, keySystem, labelParams, callback) { let box = gBrowser.getNotificationBox(browser); if (box.getNotificationWithValue(notificationId)) { return; } - let msgPrefix = "emeNotifications." + notificationId + "."; - let msgId = msgPrefix + "message"; - - let message = labelParams.length ? - gNavigatorBundle.getFormattedString(msgId, labelParams) : - gNavigatorBundle.getString(msgId); - let buttons = []; - if (callback) { + if (buttonCallback) { + let msgPrefix = "emeNotifications." + notificationId + "."; let btnLabelId = msgPrefix + "button.label"; let btnAccessKeyId = msgPrefix + "button.accesskey"; buttons.push({ label: gNavigatorBundle.getString(btnLabelId), accessKey: gNavigatorBundle.getString(btnAccessKeyId), - callback + callback: buttonCallback, }); } let iconURL = "chrome://browser/skin/drm-icon.svg#chains-black"; - - // Do a little dance to get rich content into the notification: - let fragment = document.createDocumentFragment(); - let descriptionContainer = document.createElement("description"); - // eslint-disable-next-line no-unsanitized/property - descriptionContainer.innerHTML = message; - while (descriptionContainer.childNodes.length) { - fragment.appendChild(descriptionContainer.childNodes[0]); - } - - box.appendNotification(fragment, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM, + box.appendNotification(notificationMessage, notificationId, iconURL, box.PRIORITY_WARNING_MEDIUM, buttons); }, showPopupNotificationForSuccess(browser, keySystem) { diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm index 399ed94fb7fe3..ca07353aecf0f 100644 --- a/browser/components/customizableui/CustomizableWidgets.jsm +++ b/browser/components/customizableui/CustomizableWidgets.jsm @@ -421,7 +421,7 @@ const CustomizableWidgets = [ // Put it all together... let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs); // eslint-disable-next-line no-unsanitized/property - promoParentElt.innerHTML = contents; + promoParentElt.unsafeSetInnerHTML(contents); // We manually manage the "click" event to open the promo links because // allowing the "text-link" widget handle it has 2 problems: (1) it only // supports button 0 and (2) it's tricky to intercept when it does the diff --git a/browser/modules/ExtensionsUI.jsm b/browser/modules/ExtensionsUI.jsm index a7a856daa5d69..814265ea809a9 100644 --- a/browser/modules/ExtensionsUI.jsm +++ b/browser/modules/ExtensionsUI.jsm @@ -487,10 +487,10 @@ this.ExtensionsUI = { let doc = this.browser.ownerDocument; // eslint-disable-next-line no-unsanitized/property doc.getElementById("addon-installed-notification-header") - .innerHTML = msg1; + .unsafeSetInnerHTML(msg1); // eslint-disable-next-line no-unsanitized/property doc.getElementById("addon-installed-notification-message") - .innerHTML = msg2; + .unsafeSetInnerHTML(msg2); } else if (topic == "dismissed") { resolve(); } diff --git a/browser/modules/webrtcUI.jsm b/browser/modules/webrtcUI.jsm index 9c54ad9ff6c0e..31ad49f7a83fb 100644 --- a/browser/modules/webrtcUI.jsm +++ b/browser/modules/webrtcUI.jsm @@ -626,21 +626,26 @@ function prompt(aBrowser, aRequest) { bundle.getString("getUserMedia.shareScreen.learnMoreLabel"); let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL"); - let learnMore = - ""; + + let learnMore = chromeWin.document.createElement("label"); + learnMore.className = "text-link"; + learnMore.setAttribute("href", baseURL + "screenshare-safety"); + learnMore.textContent = learnMoreText; if (type == "screen") { string = bundle.getFormattedString("getUserMedia.shareScreenWarning.message", - [learnMore]); + ["<>"]); } else { let brand = doc.getElementById("bundle_brand").getString("brandShortName"); string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message", - [brand, learnMore]); + [brand, "<>"]); } - // eslint-disable-next-line no-unsanitized/property - warning.innerHTML = string; + + let [pre, post] = string.split("<>"); + warning.textContent = pre; + warning.appendChild(learnMore); + warning.appendChild(chromeWin.document.createTextNode(post)); } let perms = Services.perms; diff --git a/devtools/client/responsive.html/components/browser.js b/devtools/client/responsive.html/components/browser.js index d529bbbf2fd70..6395ff5ebf32d 100644 --- a/devtools/client/responsive.html/components/browser.js +++ b/devtools/client/responsive.html/components/browser.js @@ -15,6 +15,12 @@ const { DOM: dom, createClass, addons, PropTypes } = const e10s = require("../utils/e10s"); const message = require("../utils/message"); +// Allow creation of HTML fragments without automatic sanitization, even +// though we're in a chrome-privileged document. +// This is, unfortunately, necessary in order to React to function +// correctly. +document.allowUnsafeHTML = true; + module.exports = createClass({ /** diff --git a/devtools/shared/gcli/source/lib/gcli/util/util.js b/devtools/shared/gcli/source/lib/gcli/util/util.js index 650e84b8f1f68..a9f9475dbfc13 100644 --- a/devtools/shared/gcli/source/lib/gcli/util/util.js +++ b/devtools/shared/gcli/source/lib/gcli/util/util.js @@ -498,7 +498,11 @@ exports.setContents = function(elem, contents) { return; } - if ('innerHTML' in elem) { + if ('unsafeSetInnerHTML' in elem) { + // FIXME: Stop relying on unsanitized HTML. + elem.unsafeSetInnerHTML(contents); + } + else if ('innerHTML' in elem) { elem.innerHTML = contents; } else { diff --git a/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js index 9fc2264671c7f..ecf5c52ad2090 100644 --- a/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js +++ b/devtools/shared/tests/browser/browser_l10n_localizeMarkup.js @@ -18,7 +18,7 @@ add_task(function* () { info("Create the test markup"); let div = document.createElement("div"); - div.innerHTML = + div.unsafeSetInnerHTML( `
Text will disappear
@@ -32,7 +32,7 @@ add_task(function* () {
- `; + `); info("Use localization helper to localize the test markup"); localizeMarkup(div); diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp index fffc66f222fe5..661e1aaf70484 100644 --- a/dom/base/Element.cpp +++ b/dom/base/Element.cpp @@ -3722,6 +3722,12 @@ Element::SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError) SetInnerHTMLInternal(aInnerHTML, aError); } +void +Element::UnsafeSetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError) +{ + SetInnerHTMLInternal(aInnerHTML, aError, true); +} + void Element::GetOuterHTML(nsAString& aOuterHTML) { diff --git a/dom/base/Element.h b/dom/base/Element.h index d4248444f1b14..50a19ea5bb914 100644 --- a/dom/base/Element.h +++ b/dom/base/Element.h @@ -1128,6 +1128,7 @@ class Element : public FragmentOrElement NS_IMETHOD GetInnerHTML(nsAString& aInnerHTML); virtual void SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError); + void UnsafeSetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError); void GetOuterHTML(nsAString& aOuterHTML); void SetOuterHTML(const nsAString& aOuterHTML, ErrorResult& aError); void InsertAdjacentHTML(const nsAString& aPosition, const nsAString& aText, diff --git a/dom/base/FragmentOrElement.cpp b/dom/base/FragmentOrElement.cpp index 608241975b452..6edfc3f34f07e 100644 --- a/dom/base/FragmentOrElement.cpp +++ b/dom/base/FragmentOrElement.cpp @@ -2388,7 +2388,8 @@ ContainsMarkup(const nsAString& aStr) } void -FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError) +FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError, + bool aNeverSanitize) { FragmentOrElement* target = this; // Handle template case. @@ -2442,6 +2443,9 @@ FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult contextNameSpaceID = shadowRoot->GetHost()->GetNameSpaceID(); } + auto sanitize = (aNeverSanitize ? nsContentUtils::NeverSanitize + : nsContentUtils::SanitizeSystemPrivileged); + if (doc->IsHTMLDocument()) { int32_t oldChildCount = target->GetChildCount(); aError = nsContentUtils::ParseFragmentHTML(aInnerHTML, @@ -2450,14 +2454,17 @@ FragmentOrElement::SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult contextNameSpaceID, doc->GetCompatibilityMode() == eCompatibility_NavQuirks, - true); + true, + sanitize); mb.NodesAdded(); // HTML5 parser has notified, but not fired mutation events. nsContentUtils::FireMutationEventsForDirectParsing(doc, target, oldChildCount); } else { RefPtr df = - nsContentUtils::CreateContextualFragment(target, aInnerHTML, true, aError); + nsContentUtils::CreateContextualFragment(target, aInnerHTML, true, + sanitize, + aError); if (!aError.Failed()) { // Suppress assertion about node removal mutation events that can't have // listeners anyway, because no one has had the chance to register mutation diff --git a/dom/base/FragmentOrElement.h b/dom/base/FragmentOrElement.h index f0e6684416e36..43df06a1becae 100644 --- a/dom/base/FragmentOrElement.h +++ b/dom/base/FragmentOrElement.h @@ -375,7 +375,8 @@ class FragmentOrElement : public nsIContent protected: void GetMarkup(bool aIncludeSelf, nsAString& aMarkup); - void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError); + void SetInnerHTMLInternal(const nsAString& aInnerHTML, ErrorResult& aError, + bool aNeverSanitize = false); // Override from nsINode virtual nsINode::nsSlots* CreateSlots() override; diff --git a/dom/base/nsContentUtils.cpp b/dom/base/nsContentUtils.cpp index 0401c1e1de3d4..eaa092ef736f1 100644 --- a/dom/base/nsContentUtils.cpp +++ b/dom/base/nsContentUtils.cpp @@ -160,6 +160,7 @@ #include "nsIOfflineCacheUpdate.h" #include "nsIParser.h" #include "nsIParserService.h" +#include "nsIParserUtils.h" #include "nsIPermissionManager.h" #include "nsIPluginHost.h" #include "nsIRequest.h" @@ -200,6 +201,7 @@ #include "nsTextFragment.h" #include "nsTextNode.h" #include "nsThreadUtils.h" +#include "nsTreeSanitizer.h" #include "nsUnicodeProperties.h" #include "nsViewManager.h" #include "nsViewportInfo.h" @@ -4916,6 +4918,7 @@ already_AddRefed nsContentUtils::CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment, bool aPreventScriptExecution, + SanitizeFragments aSanitize, ErrorResult& aRv) { if (!aContextNode) { @@ -4951,14 +4954,16 @@ nsContentUtils::CreateContextualFragment(nsINode* aContextNode, contextAsContent->GetNameSpaceID(), (document->GetCompatibilityMode() == eCompatibility_NavQuirks), - aPreventScriptExecution); + aPreventScriptExecution, + aSanitize); } else { aRv = ParseFragmentHTML(aFragment, frag, nsGkAtoms::body, kNameSpaceID_XHTML, (document->GetCompatibilityMode() == eCompatibility_NavQuirks), - aPreventScriptExecution); + aPreventScriptExecution, + aSanitize); } return frag.forget(); @@ -5022,7 +5027,8 @@ nsContentUtils::CreateContextualFragment(nsINode* aContextNode, nsCOMPtr frag; aRv = ParseFragmentXML(aFragment, document, tagStack, - aPreventScriptExecution, getter_AddRefs(frag)); + aPreventScriptExecution, getter_AddRefs(frag), + aSanitize); return frag.forget().downcast(); } @@ -5049,7 +5055,8 @@ nsContentUtils::ParseFragmentHTML(const nsAString& aSourceBuffer, nsIAtom* aContextLocalName, int32_t aContextNamespace, bool aQuirks, - bool aPreventScriptExecution) + bool aPreventScriptExecution, + SanitizeFragments aSanitize) { AutoTimelineMarker m(aTargetNode->OwnerDoc()->GetDocShell(), "Parse HTML"); @@ -5063,13 +5070,41 @@ nsContentUtils::ParseFragmentHTML(const nsAString& aSourceBuffer, NS_ADDREF(sHTMLFragmentParser = new nsHtml5StringParser()); // Now sHTMLFragmentParser owns the object } + + nsIContent* target = aTargetNode; + + // If this is a chrome-privileged document, create a fragment first, and + // sanitize it before insertion. + RefPtr fragment; + if (aSanitize != NeverSanitize && !aTargetNode->OwnerDoc()->AllowUnsafeHTML()) { + fragment = new DocumentFragment(aTargetNode->OwnerDoc()->NodeInfoManager()); + target = fragment; + } + nsresult rv = sHTMLFragmentParser->ParseFragment(aSourceBuffer, - aTargetNode, + target, aContextLocalName, aContextNamespace, aQuirks, aPreventScriptExecution); + + NS_ENSURE_SUCCESS(rv, rv); + + if (fragment) { + // Don't fire mutation events for nodes removed by the sanitizer. + nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; + + nsTreeSanitizer sanitizer(nsIParserUtils::SanitizerAllowStyle | + nsIParserUtils::SanitizerAllowComments); + sanitizer.Sanitize(fragment); + + ErrorResult error; + aTargetNode->AppendChild(*fragment, error); + rv = error.StealNSResult(); + } + + return rv; } @@ -5104,7 +5139,8 @@ nsContentUtils::ParseFragmentXML(const nsAString& aSourceBuffer, nsIDocument* aDocument, nsTArray& aTagStack, bool aPreventScriptExecution, - nsIDOMDocumentFragment** aReturn) + nsIDOMDocumentFragment** aReturn, + SanitizeFragments aSanitize) { AutoTimelineMarker m(aDocument->GetDocShell(), "Parse XML"); @@ -5143,6 +5179,20 @@ nsContentUtils::ParseFragmentXML(const nsAString& aSourceBuffer, rv = sXMLFragmentSink->FinishFragmentParsing(aReturn); sXMLFragmentParser->Reset(); + NS_ENSURE_SUCCESS(rv, rv); + + // If this is a chrome-privileged document, sanitize the fragment before + // returning. + if (aSanitize != NeverSanitize && !aDocument->AllowUnsafeHTML()) { + // Don't fire mutation events for nodes removed by the sanitizer. + nsAutoScriptBlockerSuppressNodeRemoved scriptBlocker; + + RefPtr fragment = static_cast(*aReturn); + + nsTreeSanitizer sanitizer(nsIParserUtils::SanitizerAllowStyle | + nsIParserUtils::SanitizerAllowComments); + sanitizer.Sanitize(fragment); + } return rv; } diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h index fe7b5ab0ef088..8925be83107bc 100644 --- a/dom/base/nsContentUtils.h +++ b/dom/base/nsContentUtils.h @@ -1499,6 +1499,11 @@ class nsContentUtils */ static bool IsValidNodeName(nsIAtom *aLocalName, nsIAtom *aPrefix, int32_t aNamespaceID); + + enum SanitizeFragments { + SanitizeSystemPrivileged, + NeverSanitize, + }; /** * Creates a DocumentFragment from text using a context node to resolve @@ -1513,6 +1518,8 @@ class nsContentUtils * @param aFragment the string which is parsed to a DocumentFragment * @param aReturn the resulting fragment * @param aPreventScriptExecution whether to mark scripts as already started + * @param aSanitize whether the fragment should be sanitized prior to + * injection */ static nsresult CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment, @@ -1521,7 +1528,16 @@ class nsContentUtils static already_AddRefed CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment, bool aPreventScriptExecution, + SanitizeFragments aSanitize, mozilla::ErrorResult& aRv); + static already_AddRefed + CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment, + bool aPreventScriptExecution, + mozilla::ErrorResult& aRv) + { + return CreateContextualFragment(aContextNode, aFragment, aPreventScriptExecution, + SanitizeSystemPrivileged, aRv); + } /** * Invoke the fragment parsing algorithm (innerHTML) using the HTML parser. @@ -1534,6 +1550,8 @@ class nsContentUtils * @param aPreventScriptExecution true to prevent scripts from executing; * don't set to false when parsing into a target node that has been * bound to tree. + * @param aSanitize whether the fragment should be sanitized prior to + * injection * @return NS_ERROR_DOM_INVALID_STATE_ERR if a re-entrant attempt to parse * fragments is made, NS_ERROR_OUT_OF_MEMORY if aSourceBuffer is too * long and NS_OK otherwise. @@ -1543,7 +1561,8 @@ class nsContentUtils nsIAtom* aContextLocalName, int32_t aContextNamespace, bool aQuirks, - bool aPreventScriptExecution); + bool aPreventScriptExecution, + SanitizeFragments aSanitize = SanitizeSystemPrivileged); /** * Invoke the fragment parsing algorithm (innerHTML) using the XML parser. @@ -1553,6 +1572,8 @@ class nsContentUtils * @param aTagStack the namespace mapping context * @param aPreventExecution whether to mark scripts as already started * @param aReturn the result fragment + * @param aSanitize whether the fragment should be sanitized prior to + * injection * @return NS_ERROR_DOM_INVALID_STATE_ERR if a re-entrant attempt to parse * fragments is made, a return code from the XML parser. */ @@ -1560,7 +1581,8 @@ class nsContentUtils nsIDocument* aDocument, nsTArray& aTagStack, bool aPreventScriptExecution, - nsIDOMDocumentFragment** aReturn); + nsIDOMDocumentFragment** aReturn, + SanitizeFragments aSanitize = SanitizeSystemPrivileged); /** * Parse a string into a document using the HTML parser. diff --git a/dom/base/nsDocument.cpp b/dom/base/nsDocument.cpp index b46c6b3502de0..0eb5bb9fbdb27 100644 --- a/dom/base/nsDocument.cpp +++ b/dom/base/nsDocument.cpp @@ -1354,6 +1354,7 @@ nsIDocument::nsIDocument() mIsContentDocument(false), mMightHaveStaleServoData(false), mBufferingCSPViolations(false), + mAllowUnsafeHTML(false), mIsScopedStyleEnabled(eScopedStyle_Unknown), mCompatMode(eCompatibility_FullStandards), mReadyState(ReadyState::READYSTATE_UNINITIALIZED), @@ -6095,6 +6096,13 @@ nsDocument::CustomElementConstructor(JSContext* aCx, unsigned aArgc, JS::Value* return true; } +bool +nsIDocument::AllowUnsafeHTML() const +{ + return (!nsContentUtils::IsSystemPrincipal(NodePrincipal()) || + mAllowUnsafeHTML); +} + bool nsDocument::IsWebComponentsEnabled(JSContext* aCx, JSObject* aObject) { diff --git a/dom/base/nsIDocument.h b/dom/base/nsIDocument.h index 13a61d84f5681..a9d56b2166303 100644 --- a/dom/base/nsIDocument.h +++ b/dom/base/nsIDocument.h @@ -2803,6 +2803,8 @@ class nsIDocument : public nsINode, CreateAttributeNS(const nsAString& aNamespaceURI, const nsAString& aQualifiedName, mozilla::ErrorResult& rv); + void SetAllowUnsafeHTML(bool aAllow) { mAllowUnsafeHTML = aAllow; } + bool AllowUnsafeHTML() const; void GetInputEncoding(nsAString& aInputEncoding) const; already_AddRefed GetLocation() const; void GetReferrer(nsAString& aReferrer) const; @@ -3384,6 +3386,10 @@ class nsIDocument : public nsINode, // True if any CSP violation reports for this doucment will be buffered in // mBufferedCSPViolations instead of being sent immediately. bool mBufferingCSPViolations : 1; + + // True if unsafe HTML fragments should be allowed in chrome-privileged + // documents. + bool mAllowUnsafeHTML : 1; // Whether