Bug 1415352: Part 6 - Test triggering principals and CSP subjection for inline <style> nodes. r?bz draft
authorKris Maglione <maglione.k@gmail.com>
Tue, 07 Nov 2017 15:21:25 -0800
changeset 694625 d40311d6953f8ec353703f457bf0b99a8d701fcd
parent 694624 075e525f61637c0a13278590fee96213fb7b6b49
child 739376 d219b0f287c24357725202dae47d1595f5a72907
push id88175
push usermaglione.k@gmail.com
push dateTue, 07 Nov 2017 23:59:46 +0000
reviewersbz
bugs1415352
milestone58.0a1
Bug 1415352: Part 6 - Test triggering principals and CSP subjection for inline <style> nodes. r?bz MozReview-Commit-ID: J5ZpYKno1pL
toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contentscript_triggeringPrincipal.js
@@ -92,18 +92,18 @@ const AUTOCLOSE_TAGS = new Set(["img", "
  */
 
 /**
  * Options for this specific configuration of an element test.
  *
  * @typedef {object} ElementTestOptions
  * @property {string} origin
  *        The origin with which the content is expected to load. This
- *        may be either "page" or "extension". The actual load of the
- *        URL will be tested against the computed origin strings for
+ *        may be one of "page", "contentScript", or "extension". The actual load
+ *        of the URL will be tested against the computed origin strings for
  *        those two contexts.
  * @property {string} source
  *        An arbitrary string which uniquely identifies the source of
  *        the load. For instance, each of these should have separate
  *        origin strings:
  *
  *         - An element present in the initial page HTML.
  *         - An element injected by a page script belonging to web
@@ -310,52 +310,125 @@ function testInlineCSS() {
 
   // Note: When accessing an element through `wrappedJSObject`, the operations
   // occur in the content page context, using the content subject principal.
   // When accessing it through X-ray wrappers, they happen in the content script
   // context, using its subject principal.
 
   {
     let li = document.createElement("li");
-    li.setAttribute("style", source("extension", `background: ${url("extension", "li.style-first")}`));
+    li.setAttribute("style", source("contentScript", `background: ${url("contentScript", "li.style-first")}`));
     li.style.wrappedJSObject.listStyleImage = url("page", "li.style.listStyleImage-second");
     document.body.appendChild(li);
   }
 
   {
     let li = document.createElement("li");
     li.wrappedJSObject.setAttribute("style", source("page", `background: ${url("page", "li.style-first", {inline: true})}`));
-    li.style.listStyleImage = url("extension", "li.style.listStyleImage-second");
+    li.style.listStyleImage = url("contentScript", "li.style.listStyleImage-second");
     document.body.appendChild(li);
   }
 
   {
     let li = document.createElement("li");
     document.body.appendChild(li);
-    li.setAttribute("style", source("extension", `background: ${url("extension", "li.style-first")}`));
+    li.setAttribute("style", source("contentScript", `background: ${url("contentScript", "li.style-first")}`));
     later(() => li.wrappedJSObject.setAttribute("style", source("page", `background: ${url("page", "li.style-second", {inline: true})}`)));
   }
 
   {
     let li = document.createElement("li");
     document.body.appendChild(li);
     li.wrappedJSObject.setAttribute("style", source("page", `background: ${url("page", "li.style-first", {inline: true})}`));
-    later(() => li.setAttribute("style", source("extension", `background: ${url("extension", "li.style-second")}`)));
+    later(() => li.setAttribute("style", source("contentScript", `background: ${url("contentScript", "li.style-second")}`)));
   }
 
   {
     let li = document.createElement("li");
     document.body.appendChild(li);
-    li.style.cssText = source("extension", `background: ${url("extension", "li.style.cssText-first")}`);
+    li.style.cssText = source("contentScript", `background: ${url("contentScript", "li.style.cssText-first")}`);
 
     // TODO: This inline style should be blocked, since our style-src does not
     // include 'unsafe-eval', but that is currently unimplemented.
     later(() => { li.style.wrappedJSObject.cssText = `background: ${url("page", "li.style.cssText-second")}`; });
   }
 
+  // Creates a new element, inserts it into the page, and returns its CSS selector.
+  let divNum = 0;
+  function getSelector() {
+    let div = document.createElement("div");
+    div.id = `generated-div-${divNum++}`;
+    document.body.appendChild(div);
+    return `#${div.id}`;
+  }
+
+  for (let prop of ["textContent", "innerHTML"]) {
+    // Test creating <style> element from the extension side and then replacing
+    // its contents from the content side.
+    {
+      let sel = getSelector();
+      let style = document.createElement("style");
+      style[prop] = source("extension", `${sel} { background: ${url("extension", `style-${prop}-first`)}; }`);
+      document.head.appendChild(style);
+
+      later(() => {
+        style.wrappedJSObject[prop] = source("page", `${sel} { background: ${url("page", `style-${prop}-second`, {inline: true})}; }`);
+      });
+    }
+
+    // Test creating <style> element from the extension side and then appending
+    // a text node to it. Regardless of whether the append happens from the
+    // content or extension side, this should cause the principal to be
+    // forgotten.
+    let testModifyAfterInject = (name, modifyFunc) => {
+      let sel = getSelector();
+      let style = document.createElement("style");
+      style[prop] = source("extension", `${sel} { background: ${url("extension", `style-${name}-${prop}-first`)}; }`);
+      document.head.appendChild(style);
+
+      later(() => {
+        modifyFunc(style, `${sel} { background: ${url("page", `style-${name}-${prop}-second`, {inline: true})}; }`);
+        source("page", style.textContent);
+      });
+    };
+
+    testModifyAfterInject("appendChild", (style, css) => {
+      style.appendChild(document.createTextNode(css));
+    });
+
+    // Test creating <style> element from the extension side and then appending
+    // to it using insertAdjacentHTML, with the same rules as above.
+    testModifyAfterInject("insertAdjacentHTML", (style, css) => {
+      // eslint-disable-next-line no-unsanitized/method
+      style.insertAdjacentHTML("beforeend", css);
+    });
+
+    // And again using insertAdjacentText.
+    // to it using insertAdjacentHTML, with the same rules as above.
+    testModifyAfterInject("insertAdjacentText", (style, css) => {
+      style.insertAdjacentText("beforeend", css);
+    });
+
+    // Test creating a script and then accessing its CSSStyleSheet object.
+    {
+      let sel = getSelector();
+      let style = document.createElement("style");
+      style[prop] = source("extension", `${sel} { background: ${url("extension", `style-${prop}-sheet`)}; }`);
+      document.head.appendChild(style);
+
+      browser.test.assertThrows(
+        () => style.sheet.wrappedJSObject.cssRules,
+        /operation is insecure/,
+        "Page content should not be able to access extension-generated CSS rules");
+
+      style.sheet.insertRule(
+        source("extension", `${sel} { border-image: ${url("extension", `style-${prop}-sheet-insertRule`)}; }`));
+    }
+  }
+
   setTimeout(() => {
     for (let fn of laters) {
       fn();
     }
     browser.test.sendMessage("css-sources", {urls, sources});
   });
 }
 
@@ -386,27 +459,33 @@ function injectElements(tests, baseOpts)
     let img = document.createElement("img");
     img.src = "data:image/svg+xml,%3Csvg%2F%3E";
     document.body.appendChild(img);
 
     let rand = Math.random();
 
     // Basic smoke test to check that we don't try to create stylesheets with an
     // expanded principal, which would cause a crash when loading font sets.
-    let link = document.createElement("link");
-    link.rel = "stylesheet";
-    link.href = "data:text/css;base64," + btoa(`
+    let cssText = `
       @font-face {
           font-family: "DoesNotExist${rand}";
           src: url("fonts/DoesNotExist.${rand}.woff") format("woff");
           font-weight: normal;
           font-style: normal;
-      }`);
+      }`;
+
+    let link = document.createElement("link");
+    link.rel = "stylesheet";
+    link.href = "data:text/css;base64," + btoa(cssText);
     document.head.appendChild(link);
 
+    let style = document.createElement("style");
+    style.textContent = cssText;
+    document.head.appendChild(style);
+
     let overrideOpts = opts => Object.assign({}, baseOpts, opts);
     let opts = baseOpts;
 
     // Build the full element with setAttr, then inject.
     for (let test of tests) {
       let {elem, srcElem, src} = createElement(test, opts);
       srcElem.setAttribute(test.srcAttr, src);
       document.body.appendChild(elem);
@@ -609,26 +688,26 @@ function computeBaseURLs(tests, expected
  * injected by our content script.
  *
  * @param {object} message
  *        The "css-sources" message sent by the content script, containing lists
  *        of CSS sources injected into the page.
  * @param {Array<object>} message.urls
  *        A list of URLs present in styles injected by the content script.
  * @param {string} message.urls.*.origin
- *        The origin of the URL, either "page" or "extension".
+ *        The origin of the URL, one of "page", "contentScript", or "extension".
  * @param {string} message.urls.*.href
  *        The URL string.
  * @param {boolean} message.urls.*.inline
  *        If true, the URL is present in an inline stylesheet, which may be
  *        blocked by CSP prior to parsing, depending on its origin.
  * @param {Array<object>} message.sources
  *        A list of inline CSS sources injected by the content script.
  * @param {string} message.sources.*.origin
- *        The origin of the CSS, either "page" or "extension".
+ *        The origin of the CSS, one of "page", "contentScript", or "extension".
  * @param {string} message.sources.*.css
  *        The CSS source text.
  * @param {boolean} [cspEnabled = false]
  *        If true, a strict CSP is enabled for this page, and inline page
  *        sources should be blocked. URLs present in these sources will not be
  *        expected to generate a CSP report, the inline sources themselves will.
  * @returns {RequestedURLs}
  */
@@ -921,17 +1000,17 @@ const EXTENSION_DATA = {
     content_scripts: [{
       "matches": ["http://*/page.html"],
       "run_at": "document_start",
       "js": ["content_script.js"],
     }],
   },
 
   files: {
-    "content_script.js": getInjectionScript(TESTS, {source: "contentScript", origin: "extension"}),
+    "content_script.js": getInjectionScript(TESTS, {source: "contentScript", origin: "contentScript"}),
   },
 };
 
 const pageURL = `${BASE_URL}/page.html`;
 const pageURI = Services.io.newURI(pageURL);
 
 // Merges the sets of expected URL and source data returned by separate
 // computedExpectedForbiddenURLs and computedBaseURLs calls.
@@ -939,34 +1018,41 @@ function mergeSources(a, b) {
   return {
     expectedURLs: new Set([...a.expectedURLs, ...b.expectedURLs]),
     forbiddenURLs: new Set([...a.forbiddenURLs, ...b.forbiddenURLs]),
     blockedURLs: new Set([...a.blockedURLs, ...b.blockedURLs]),
     blockedSources: a.blockedSources || b.blockedSources,
   };
 }
 
+// Returns a set of origin strings for the given extension and content page, for
+// use in verifying request triggering principals.
+function getOrigins(extension) {
+  return {
+    page: Services.scriptSecurityManager.createCodebasePrincipal(pageURI, {}).origin,
+    contentScript: Cu.getObjectPrincipal(Cu.Sandbox([extension.principal, pageURL])).origin,
+    extension: extension.principal.origin,
+  };
+}
+
 /**
  * Tests that various types of inline content elements initiate requests
  * with the triggering pringipal of the caller that requested the load.
  */
 add_task(async function test_contentscript_triggeringPrincipals() {
   let extension = ExtensionTestUtils.loadExtension(EXTENSION_DATA);
   await extension.startup();
 
   let urlsPromise = extension.awaitMessage("css-sources").then(msg => {
     return mergeSources(
       computeExpectedForbiddenURLs(msg),
       computeBaseURLs(TESTS, SOURCES));
   });
 
-  let origins = {
-    page: Services.scriptSecurityManager.createCodebasePrincipal(pageURI, {}).origin,
-    extension: Cu.getObjectPrincipal(Cu.Sandbox([extension.extension.principal, pageURL])).origin,
-  };
+  let origins = getOrigins(extension.extension);
   let finished = awaitLoads(urlsPromise, origins);
 
   let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);
 
   await finished;
 
   await extension.unload();
   await contentPage.close();
@@ -992,20 +1078,17 @@ add_task(async function test_contentscri
   await extension.startup();
 
   let urlsPromise = extension.awaitMessage("css-sources").then(msg => {
     return mergeSources(
       computeExpectedForbiddenURLs(msg, true),
       computeBaseURLs(TESTS, EXTENSION_SOURCES, PAGE_SOURCES));
   });
 
-  let origins = {
-    page: Services.scriptSecurityManager.createCodebasePrincipal(pageURI, {}).origin,
-    extension: Cu.getObjectPrincipal(Cu.Sandbox([extension.extension.principal, pageURL])).origin,
-  };
+  let origins = getOrigins(extension.extension);
 
   let finished = Promise.all([
     awaitLoads(urlsPromise, origins),
     checkCSPReports && awaitCSP(urlsPromise),
   ]);
 
   let contentPage = await ExtensionTestUtils.loadContentPage(pageURL);