[tor-commits] [tor-browser/tor-browser-52.6.0esr-8.0-2] Bug 25147: Sanitize HTML fragments created for chrome-privileged documents

gk at torproject.org gk at torproject.org
Thu Mar 8 09:20:13 UTC 2018


commit 3eb8f10e0c16c52a1d586e190e82009041535503
Author: Richard Pospesel <richard at torproject.org>
Date:   Thu Mar 1 14:12:08 2018 -0800

    Bug 25147: Sanitize HTML fragments created for chrome-privileged documents
    
    Ported over firefox patch c2db4a50dc5c (Bug 1432966)
---
 .../tests/mochitest/events/test_mutation.html      |   4 +-
 browser/base/content/browser-media.js              |  59 ++++++------
 .../customizableui/CustomizableWidgets.jsm         |   2 +-
 browser/modules/webrtcUI.jsm                       |  21 +++--
 devtools/shared/gcli/source/lib/gcli/util/util.js  |   6 +-
 .../tests/browser/browser_l10n_localizeMarkup.js   |   4 +-
 dom/base/Element.cpp                               |   6 ++
 dom/base/Element.h                                 |   1 +
 dom/base/FragmentOrElement.cpp                     |  13 ++-
 dom/base/FragmentOrElement.h                       |   3 +-
 dom/base/nsContentUtils.cpp                        |  60 ++++++++++--
 dom/base/nsContentUtils.h                          |  26 +++++-
 dom/base/nsDocument.cpp                            |  12 ++-
 dom/base/nsIDocument.h                             |   6 ++
 dom/base/test/chrome.ini                           |   1 +
 dom/base/test/chrome/test_bug683852.xul            |   4 +-
 dom/base/test/test_fragment_sanitization.xul       | 101 +++++++++++++++++++++
 dom/webidl/Document.webidl                         |   5 +
 dom/webidl/Element.webidl                          |  10 ++
 layout/style/test/chrome/bug418986-2.js            |  16 +++-
 mobile/android/chrome/content/config.js            |   4 +-
 toolkit/content/tests/chrome/test_bug570192.xul    |   4 +-
 toolkit/mozapps/extensions/content/extensions.js   |   2 +-
 23 files changed, 302 insertions(+), 68 deletions(-)

diff --git a/accessible/tests/mochitest/events/test_mutation.html b/accessible/tests/mochitest/events/test_mutation.html
index 232a0972777f..63dd74ca4775 100644
--- a/accessible/tests/mochitest/events/test_mutation.html
+++ b/accessible/tests/mochitest/events/test_mutation.html
@@ -348,8 +348,8 @@
 
       this.invoke = function insertReferredElm_invoke()
       {
-        this.containerNode.innerHTML =
-          "<span id='insertReferredElms_span'></span><input aria-labelledby='insertReferredElms_span'>";
+        this.containerNode.unsafeSetInnerHTML(
+          "<span id='insertReferredElms_span'></span><input aria-labelledby='insertReferredElms_span'>");
       }
 
       this.getID = function insertReferredElm_getID()
diff --git a/browser/base/content/browser-media.js b/browser/base/content/browser-media.js
index 81e7faf17aad..c1ed8f9399fa 100644
--- a/browser/base/content/browser-media.js
+++ b/browser/base/content/browser-media.js
@@ -41,11 +41,23 @@ var gEMEHandler = {
     }
     return true;
   },
-  getLearnMoreLink: function(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 "<label class='text-link' href='" + baseURL + "drm-content'>" +
-           text + "</label>";
+    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: function({target: browser, data: data}) {
     let parsedData;
@@ -63,7 +75,8 @@ var gEMEHandler = {
 
     let notificationId;
     let buttonCallback;
-    let params = [];
+    // Notification message can be either a string or a DOM fragment.
+    let notificationMessage;
     switch (status) {
       case "available":
       case "cdm-created":
@@ -77,18 +90,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":
@@ -100,43 +113,27 @@ var gEMEHandler = {
         return;
     }
 
-    this.showNotificationBar(browser, notificationId, keySystem, params, buttonCallback);
-  },
-  showNotificationBar: function(browser, notificationId, keySystem, labelParams, callback) {
+    // Now actually create the notification
+
     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
+        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");
-    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: function(browser, keySystem) {
diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm
index 907e2e0f75b7..b18249a35811 100644
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -327,7 +327,7 @@ const CustomizableWidgets = [
       let promoParentElt = doc.getElementById("PanelUI-remotetabs-mobile-promo");
       // Put it all together...
       let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs);
-      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/webrtcUI.jsm b/browser/modules/webrtcUI.jsm
index b24135bfc959..ddcf09260f97 100644
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -536,21 +536,26 @@ function prompt(aBrowser, aRequest) {
               bundle.getString("getUserMedia.shareScreen.learnMoreLabel");
             let baseURL =
               Services.urlFormatter.formatURLPref("app.support.baseURL");
-            let learnMore =
-              "<label class='text-link' href='" + baseURL + "screenshare-safety'>" +
-              learnMoreText + "</label>";
+
+            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 {
+                                                 ["<>"]);
+            } else {
               let brand =
                 chromeDoc.getElementById("bundle_brand").getString("brandShortName");
               string = bundle.getFormattedString("getUserMedia.shareFirefoxWarning.message",
-                                                 [brand, learnMore]);
+                                                 [brand, "<>"]);
             }
-            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/shared/gcli/source/lib/gcli/util/util.js b/devtools/shared/gcli/source/lib/gcli/util/util.js
index 065bf36c07db..cfc18062e1c5 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 f33a5a331c51..59bbb308ba44 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(
   `<div data-localization-bundle="devtools/client/locales/startup.properties">
      <div id="d0" data-localization="content=inspector.someInvalidKey"></div>
      <div id="d1" data-localization="content=inspector.label">Text will disappear</div>
@@ -32,7 +32,7 @@ add_task(function* () {
        <div id="d5" data-localization="content=toolbox.defaultTitle"></div>
      </div>
    </div>
-  `;
+  `);
 
   info("Use localization helper to localize the test markup");
   localizeMarkup(div);
diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp
index 8d0edf6e7aeb..8f99077996ed 100644
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -3558,6 +3558,12 @@ Element::SetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError)
 }
 
 void
+Element::UnsafeSetInnerHTML(const nsAString& aInnerHTML, ErrorResult& aError)
+{
+  SetInnerHTMLInternal(aInnerHTML, aError, true);
+}
+
+void
 Element::GetOuterHTML(nsAString& aOuterHTML)
 {
   GetMarkup(true, aOuterHTML);
diff --git a/dom/base/Element.h b/dom/base/Element.h
index 5d878df60668..94991b26a641 100644
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -896,6 +896,7 @@ public:
 
   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 293177ce7ca9..dac32c1905be 100644
--- a/dom/base/FragmentOrElement.cpp
+++ b/dom/base/FragmentOrElement.cpp
@@ -2184,7 +2184,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.
@@ -2238,6 +2239,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,
@@ -2246,14 +2250,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<DocumentFragment> 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 3cb5575fe063..c6ed7ab85e41 100644
--- a/dom/base/FragmentOrElement.h
+++ b/dom/base/FragmentOrElement.h
@@ -357,7 +357,8 @@ public:
 
 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 f93274280f7c..182a1686d5fe 100644
--- a/dom/base/nsContentUtils.cpp
+++ b/dom/base/nsContentUtils.cpp
@@ -155,6 +155,7 @@
 #include "nsIObserverService.h"
 #include "nsIOfflineCacheUpdate.h"
 #include "nsIParser.h"
+#include "nsIParserUtils.h"
 #include "nsIParserService.h"
 #include "nsIPermissionManager.h"
 #include "nsIPluginHost.h"
@@ -195,6 +196,7 @@
 #include "nsTextFragment.h"
 #include "nsTextNode.h"
 #include "nsThreadUtils.h"
+#include "nsTreeSanitizer.h"
 #include "nsUnicharUtilCIID.h"
 #include "nsUnicodeProperties.h"
 #include "nsViewManager.h"
@@ -4576,6 +4578,7 @@ already_AddRefed<DocumentFragment>
 nsContentUtils::CreateContextualFragment(nsINode* aContextNode,
                                          const nsAString& aFragment,
                                          bool aPreventScriptExecution,
+                                         SanitizeFragments aSanitize,
                                          ErrorResult& aRv)
 {
   if (!aContextNode) {
@@ -4611,14 +4614,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();
@@ -4682,7 +4687,8 @@ nsContentUtils::CreateContextualFragment(nsINode* aContextNode,
 
   nsCOMPtr<nsIDOMDocumentFragment> frag;
   aRv = ParseFragmentXML(aFragment, document, tagStack,
-                         aPreventScriptExecution, getter_AddRefs(frag));
+                         aPreventScriptExecution, getter_AddRefs(frag),
+                         aSanitize);
   return frag.forget().downcast<DocumentFragment>();
 }
 
@@ -4709,7 +4715,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");
 
@@ -4723,13 +4730,39 @@ 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<DocumentFragment> 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;
 }
 
@@ -4764,7 +4797,8 @@ nsContentUtils::ParseFragmentXML(const nsAString& aSourceBuffer,
                                  nsIDocument* aDocument,
                                  nsTArray<nsString>& aTagStack,
                                  bool aPreventScriptExecution,
-                                 nsIDOMDocumentFragment** aReturn)
+                                 nsIDOMDocumentFragment** aReturn,
+                                 SanitizeFragments aSanitize)
 {
   AutoTimelineMarker m(aDocument->GetDocShell(), "Parse XML");
 
@@ -4803,6 +4837,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<DocumentFragment> fragment = static_cast<DocumentFragment*>(*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 08c5cbf02e25..f2de4a16be5d 100644
--- a/dom/base/nsContentUtils.h
+++ b/dom/base/nsContentUtils.h
@@ -1344,6 +1344,11 @@ public:
   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
    * namespaces.
@@ -1357,6 +1362,8 @@ public:
    * @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,
@@ -1365,7 +1372,16 @@ public:
   static already_AddRefed<mozilla::dom::DocumentFragment>
   CreateContextualFragment(nsINode* aContextNode, const nsAString& aFragment,
                            bool aPreventScriptExecution,
+                           SanitizeFragments aSanitize,
                            mozilla::ErrorResult& aRv);
+  static already_AddRefed<mozilla::dom::DocumentFragment>
+  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.
@@ -1378,6 +1394,8 @@ public:
    * @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.
@@ -1387,7 +1405,8 @@ public:
                                     nsIAtom* aContextLocalName,
                                     int32_t aContextNamespace,
                                     bool aQuirks,
-                                    bool aPreventScriptExecution);
+                                    bool aPreventScriptExecution,
+                                    SanitizeFragments aSanitize = SanitizeSystemPrivileged);
 
   /**
    * Invoke the fragment parsing algorithm (innerHTML) using the XML parser.
@@ -1397,6 +1416,8 @@ public:
    * @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.
    */
@@ -1404,7 +1425,8 @@ public:
                                    nsIDocument* aDocument,
                                    nsTArray<nsString>& 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 25e5d28d6c76..ba35493ad886 100644
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1315,7 +1315,8 @@ nsIDocument::nsIDocument()
     mFrameRequestCallbacksScheduled(false),
     mBidiOptions(IBMBIDI_DEFAULT_BIDI_OPTIONS),
     mPartID(0),
-    mUserHasInteracted(false)
+    mUserHasInteracted(false),
+    mAllowUnsafeHTML(false)
 {
   SetIsInDocument();
 
@@ -2659,7 +2660,7 @@ nsDocument::InitCSP(nsIChannel* aChannel)
     (cspSandboxFlags & SANDBOXED_ORIGIN) && !(mSandboxFlags & SANDBOXED_ORIGIN);
 
   mSandboxFlags |= cspSandboxFlags;
-  
+
   if (needNewNullPrincipal) {
     principal = nsNullPrincipal::CreateWithInheritedAttributes(principal);
     principal->SetCsp(csp);
@@ -5734,6 +5735,13 @@ nsDocument::CustomElementConstructor(JSContext* aCx, unsigned aArgc, JS::Value*
 }
 
 bool
+nsIDocument::AllowUnsafeHTML() const
+{
+  return (!nsContentUtils::IsSystemPrincipal(NodePrincipal()) ||
+          mAllowUnsafeHTML);
+}
+
+bool
 nsDocument::IsWebComponentsEnabled(JSContext* aCx, JSObject* aObject)
 {
   JS::Rooted<JSObject*> obj(aCx, aObject);
diff --git a/dom/base/nsIDocument.h b/dom/base/nsIDocument.h
index 5715fd233477..3ff69a6e8e1d 100644
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2660,6 +2660,8 @@ public:
     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<mozilla::dom::Location> GetLocation() const;
   void GetReferrer(nsAString& aReferrer) const;
@@ -3206,6 +3208,10 @@ protected:
   // UpdateFrameRequestCallbackSchedulingState.
   bool mFrameRequestCallbacksScheduled : 1;
 
+  // True if unsafe HTML fragments should be allowed in chrome-privileged
+  // documents.
+  bool mAllowUnsafeHTML : 1;
+
   enum Type {
     eUnknown, // should never be used
     eHTML,
diff --git a/dom/base/test/chrome.ini b/dom/base/test/chrome.ini
index f7e67ef6b9cb..8d4a9cd09a9c 100644
--- a/dom/base/test/chrome.ini
+++ b/dom/base/test/chrome.ini
@@ -16,6 +16,7 @@ support-files =
 [test_copypaste.xul]
 subsuite = clipboard
 [test_domrequesthelper.xul]
+[test_fragment_sanitization.xul]
 [test_messagemanager_principal.html]
 [test_messagemanager_send_principal.html]
 skip-if = buildapp == 'mulet'
diff --git a/dom/base/test/chrome/test_bug683852.xul b/dom/base/test/chrome/test_bug683852.xul
index cebc8f3583bc..e7e106f7a483 100644
--- a/dom/base/test/chrome/test_bug683852.xul
+++ b/dom/base/test/chrome/test_bug683852.xul
@@ -20,6 +20,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=683852
   /** Test for Bug 683852 **/
   SimpleTest.waitForExplicitFinish();
 
+  const NS_HTML = "http://www.w3.org/1999/xhtml";
+
   function startTest() {
     is(document.contains(document), true, "Document should contain itself!");
 
@@ -48,7 +50,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=683852
     document.documentElement.appendChild(pi);
     document.contains(pi, true, "Document should contain processing instruction");
 
-    var df = document.createRange().createContextualFragment("<div>foo</div>");
+    var df = document.createRange().createContextualFragment(`<div xmlns="${NS_HTML}">foo</div>`);
     is(df.contains(df.firstChild), true, "Document fragment should contain its child");
     is(df.contains(df.firstChild.firstChild), true,
        "Document fragment should contain its descendant");
diff --git a/dom/base/test/test_fragment_sanitization.xul b/dom/base/test/test_fragment_sanitization.xul
new file mode 100644
index 000000000000..0c91b210125c
--- /dev/null
+++ b/dom/base/test/test_fragment_sanitization.xul
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<?xml-stylesheet type="text/css" href="chrome://global/skin"?>
+<?xml-stylesheet type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1432966
+-->
+<window title="Mozilla Bug 1432966"
+        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"/>
+
+  <script type="application/javascript"><![CDATA[
+
+var { classes: Cc, interfaces: Ci } = Components;
+
+const NS_HTML = "http://www.w3.org/1999/xhtml";
+
+function awaitLoad(frame) {
+  return new Promise(resolve => {
+    frame.addEventListener("load", resolve, {once: true});
+  });
+}
+
+async function testFrame(frame, html, expected = html) {
+  document.querySelector("body").appendChild(frame);
+  await awaitLoad(frame);
+
+  // Remove the xmlns attributes that will be automatically added when we're
+  // in an XML document, and break the comparison.
+  function unNS(text) {
+    return text.replace(RegExp(` xmlns="${NS_HTML}"`, "g"), "");
+  }
+
+  let doc = frame.contentDocument;
+  let body = doc.body || doc.documentElement;
+
+  let div = doc.createElementNS(NS_HTML, "div");
+  body.appendChild(div);
+
+  div.innerHTML = html;
+  is(unNS(div.innerHTML), expected, "innerHTML value");
+
+  div.innerHTML = "<div></div>";
+  div.firstChild.outerHTML = html;
+  is(unNS(div.innerHTML), expected, "outerHTML value");
+
+  div.textContent = "";
+  div.insertAdjacentHTML("beforeend", html);
+  is(unNS(div.innerHTML), expected, "insertAdjacentHTML('beforeend') value");
+
+  div.innerHTML = "<a>foo</a>";
+  div.firstChild.insertAdjacentHTML("afterend", html);
+  is(unNS(div.innerHTML), "<a>foo</a>" + expected, "insertAdjacentHTML('afterend') value");
+
+  frame.remove();
+}
+
+add_task(async function test_fragment_sanitization() {
+  const XUL_URL = "chrome://global/content/win.xul";
+  const HTML_URL = "chrome://mochitests/content/chrome/dom/base/test/file_empty.html";
+
+  const HTML = '<a onclick="foo()" href="javascript:foo"><script>bar()<\/script>Meh.</a><a href="http://foo/"></a>';
+  const SANITIZED = '<a>Meh.</a><a href="http://foo/"></a>';
+
+  info("Test content HTML document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = "http://example.com/";
+
+    await testFrame(frame, HTML);
+  }
+
+  info("Test chrome HTML document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = HTML_URL;
+
+    await testFrame(frame, HTML, SANITIZED);
+  }
+
+  info("Test chrome XUL document");
+  {
+    let frame = document.createElementNS(NS_HTML, "iframe");
+    frame.src = XUL_URL;
+
+    await testFrame(frame, HTML, SANITIZED);
+  }
+});
+
+  ]]></script>
+
+  <description style="-moz-user-focus: normal; -moz-user-select: text;"><![CDATA[
+    hello
+    world
+  ]]></description>
+
+  <body xmlns="http://www.w3.org/1999/xhtml">
+    <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432966"
+       target="_blank">Mozilla Bug 1432966</a>
+  </body>
+</window>
diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl
index c895fad39275..ed071cbbd375 100644
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -95,6 +95,11 @@ interface Document : Node {
   Attr createAttribute(DOMString name);
   [NewObject, Throws]
   Attr createAttributeNS(DOMString? namespace, DOMString name);
+
+  // Allows setting innerHTML without automatic sanitization.
+  // Do not use this.
+  [ChromeOnly]
+  attribute boolean allowUnsafeHTML;
 };
 
 // http://www.whatwg.org/specs/web-apps/current-work/#the-document-object
diff --git a/dom/webidl/Element.webidl b/dom/webidl/Element.webidl
index ca5f1b35cd78..a66327ff96cd 100644
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -216,6 +216,16 @@ partial interface Element {
   attribute DOMString outerHTML;
   [Throws]
   void insertAdjacentHTML(DOMString position, DOMString text);
+
+  /**
+   * Like the innerHTML setter, but does not sanitize its values, even in
+   * chrome-privileged documents.
+   *
+   * If you're thinking about using this, don't. You have many, much better
+   * options.
+   */
+  [ChromeOnly, Throws]
+  void unsafeSetInnerHTML(DOMString html);
 };
 
 // http://www.w3.org/TR/selectors-api/#interface-definitions
diff --git a/layout/style/test/chrome/bug418986-2.js b/layout/style/test/chrome/bug418986-2.js
index 4336f4abdb2c..2fcad6d70e0b 100644
--- a/layout/style/test/chrome/bug418986-2.js
+++ b/layout/style/test/chrome/bug418986-2.js
@@ -222,13 +222,23 @@ var green = (function () {
   return getComputedStyle(temp).backgroundColor;
 })();
 
+// Injected HTML will automatically be sanitized when we're in a chrome
+// document unless we use `unsafeSetInnerHTML`. That function doesn't
+// exist in non-chrome documents, so add a stub to allow the same code
+// to run in both.
+if (!Element.prototype.unsafeSetInnerHTML) {
+  Element.prototype.unsafeSetInnerHTML = html => {
+    this.innerHTML = html;
+  };
+}
+
 // __testCSS(resisting)__.
 // Creates a series of divs and CSS using media queries to set their
 // background color. If all media queries match as expected, then
 // all divs should have a green background color.
 var testCSS = function (resisting) {
-  document.getElementById("display").innerHTML = generateHtmlLines(resisting);
-  document.getElementById("test-css").innerHTML = generateCSSLines(resisting);
+  document.getElementById("display").unsafeSetInnerHTML(generateHtmlLines(resisting));
+  document.getElementById("test-css").unsafeSetInnerHTML(generateCSSLines(resisting));
   let cssTestDivs = document.querySelectorAll(".spoof,.suppress");
   for (let div of cssTestDivs) {
     let color = window.getComputedStyle(div).backgroundColor;
@@ -272,7 +282,7 @@ var testMediaQueriesInPictureElements = function* (resisting) {
       lines += "</picture><br/>\n";
     }
   }
-  document.getElementById("pictures").innerHTML = lines;
+  document.getElementById("pictures").unsafeSetInnerHTML(lines);
   var testImages = document.getElementsByClassName("testImage");
   yield sleep(0);
   for (let testImage of testImages) {
diff --git a/mobile/android/chrome/content/config.js b/mobile/android/chrome/content/config.js
index 2c868f175ee8..83f93a51c86a 100644
--- a/mobile/android/chrome/content/config.js
+++ b/mobile/android/chrome/content/config.js
@@ -599,7 +599,7 @@ Pref.prototype = {
       this.li.setAttribute("contextmenu", "prefs-context-menu");
 
       // Create list item outline, bind to object actions
-      this.li.innerHTML =
+      this.li.unsafeSetInnerHTML(
         "<div class='pref-name' " +
             "onclick='AboutConfig.selectOrToggleBoolPref(event);'>" +
             this.name +
@@ -623,7 +623,7 @@ Pref.prototype = {
           "<div class='pref-button down' " +
             "onclick='AboutConfig.incrOrDecrIntPref(event, -1);'>" +
           "</div>" +
-        "</div>";
+        "</div>");
 
       // Delay providing the list item values, until the LI is returned and added to the document
       setTimeout(this._valueSetup.bind(this), INNERHTML_VALUE_DELAY);
diff --git a/toolkit/content/tests/chrome/test_bug570192.xul b/toolkit/content/tests/chrome/test_bug570192.xul
index 09f73e932b5d..91c55805c9da 100644
--- a/toolkit/content/tests/chrome/test_bug570192.xul
+++ b/toolkit/content/tests/chrome/test_bug570192.xul
@@ -36,8 +36,8 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=570192
     addLoadEvent(function() {
       try {
         var content = document.getElementById("content");
-        content.innerHTML = '<textbox newlines="pasteintact" ' +
-          'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>';
+        content.unsafeSetInnerHTML('<textbox newlines="pasteintact" ' +
+          'xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"/>');
         var textbox = content.firstChild;
         ok(textbox, "created the textbox");
         ok(!textbox.editor, "do we have an editor?");
diff --git a/toolkit/mozapps/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js
index 1e5f33b0a251..771008ed3bf8 100644
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -3078,7 +3078,7 @@ var gDetailView = {
       // plugins without having bug 624602 fixed yet, and intentionally ignores
       // localisation.
       if (aAddon.isGMPlugin) {
-        fullDesc.innerHTML = aAddon.fullDescription;
+        fullDesc.unsafeSetInnerHTML(aAddon.fullDescription);
       } else {
         fullDesc.textContent = aAddon.fullDescription;
       }





More information about the tor-commits mailing list