From 891e76d9b5e1800bfbe9aaae1fa822940260ece4 Mon Sep 17 00:00:00 2001
From: mkwst <mkwst@chromium.org>
Date: Tue, 16 May 2017 14:18:45 -0700
Subject: [PATCH] Move `<script nonce>` hiding to `Element`.

We're evaluating a different approach to hiding the `nonce` content
attribute, moving the behavior change up to `HTMLElement` and `SVGElement`
rather than placing it on `{HTML,SVG}{Script,Style}Element`. This
patch adds `nonce` to `ElementRareData` in order to support that
approach, and wires up a new `NoncedElement` interface to the new
properties.

Still behind a flag while we're working out details.

Intent to Implement and Ship: https://groups.google.com/a/chromium.org/forum/#!msg/blink-dev/wu_fMIYkyaQ/85j16Cg6BAAJ

BUG=680419

Review-Url: https://codereview.chromium.org/2801243002
Cr-Commit-Position: refs/heads/master@{#472215}
---
 .../script-nonces-hidden-meta.html            | 116 +++++++++++++++++
 .../_unapproved/script-nonces-hidden.html     | 114 ++++++++++++++++
 .../script-nonces-hidden.html.headers         |   1 +
 .../svgscript-nonces-hidden-meta.html         | 122 ++++++++++++++++++
 .../_unapproved/svgscript-nonces-hidden.html  | 122 ++++++++++++++++++
 .../svgscript-nonces-hidden.html.headers      |   1 +
 6 files changed, 476 insertions(+)
 create mode 100644 content-security-policy/_unapproved/script-nonces-hidden-meta.html
 create mode 100644 content-security-policy/_unapproved/script-nonces-hidden.html
 create mode 100644 content-security-policy/_unapproved/script-nonces-hidden.html.headers
 create mode 100644 content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html
 create mode 100644 content-security-policy/_unapproved/svgscript-nonces-hidden.html
 create mode 100644 content-security-policy/_unapproved/svgscript-nonces-hidden.html.headers

diff --git a/content-security-policy/_unapproved/script-nonces-hidden-meta.html b/content-security-policy/_unapproved/script-nonces-hidden-meta.html
new file mode 100644
index 00000000000000..812e50dfb9147b
--- /dev/null
+++ b/content-security-policy/_unapproved/script-nonces-hidden-meta.html
@@ -0,0 +1,116 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<meta http-equiv="content-security-policy" content="script-src 'nonce-abc'; img-src 'none'">
+
+<body>
+<!-- Basics -->
+<script nonce="abc" id="testScript">
+  document.currentScript.setAttribute('executed', 'yay');
+</script>
+
+<script nonce="abc">
+    var script = document.querySelector('#testScript');
+
+    test(t => {
+      // Query Selector
+      assert_equals(document.querySelector('body [nonce]'), script);
+      assert_equals(document.querySelector('body [nonce=""]'), null);
+      assert_equals(document.querySelector('body [nonce=abc]'), script);
+
+      assert_equals(script.getAttribute('nonce'), 'abc');
+      assert_equals(script.nonce, 'abc');
+    }, "Reading 'nonce' content attribute and IDL attribute.");
+
+    // Clone node.
+    test(t => {
+      script.setAttribute('executed', 'boo');
+      var s2 = script.cloneNode();
+      assert_equals(s2.nonce, 'abc', 'IDL attribute');
+      assert_equals(s2.getAttribute('nonce'), 'abc');
+    }, "Cloned node retains nonce.");
+
+    async_test(t => {
+      var s2 = script.cloneNode();
+      document.head.appendChild(s2);
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s2.nonce, 'abc');
+        assert_equals(s2.getAttribute('nonce'), 'abc');
+
+        // The cloned script won't execute, as its 'already started' flag is set.
+        assert_equals(s2.getAttribute('executed'), 'boo');
+      }));
+    }, "Cloned node retains nonce when inserted.");
+
+    // Set the content attribute to 'foo'
+    test(t => {
+      script.setAttribute('nonce', 'foo');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+      assert_equals(script.nonce, 'abc');
+    }, "Writing 'nonce' content attribute.");
+
+    // Set the IDL attribute to 'bar'
+    test(t => {
+      script.nonce = 'bar';
+      assert_equals(script.nonce, 'bar');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+    }, "Writing 'nonce' IDL attribute.");
+
+    // Fragment parser.
+    var documentWriteTest = async_test("Document-written script executes.");
+    document.write(`<script nonce='abc'>
+      documentWriteTest.done();
+      test(t => {
+        var script = document.currentScript;
+        assert_equals(script.getAttribute('nonce'), 'abc');
+        assert_equals(script.nonce, 'abc');
+      }, "Document-written script's nonce value.");
+    </scr` + `ipt>`);
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('script');
+      s.innerText = script.innerText;
+      s.nonce = 'abc';
+      document.head.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s.nonce, 'abc');
+        assert_equals(s.getAttribute('nonce'), null);
+        assert_equals(s.getAttribute('executed'), 'yay');
+      }));
+    }, "createElement.nonce.");
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('script');
+      s.innerText = script.innerText;
+      s.setAttribute('nonce', 'abc');
+      assert_equals(s.getAttribute('nonce'), 'abc', "Pre-insertion content");
+      assert_equals(s.nonce, '', "Pre-insertion IDL");
+      document.head.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s.nonce, 'abc', "Post-insertion IDL");
+        assert_equals(s.getAttribute('nonce'), 'abc', "Post-insertion content");
+        assert_equals(s.getAttribute('executed'), 'yay');
+      }));
+    }, "createElement.setAttribute.");
+</script>
+
+<!-- CSS Leakage -->
+<style>
+    #cssTest { display: block; }
+    #cssTest[nonce=abc] { background: url(/security/resources/abe.png); }
+</style>
+<script nonce="abc" id="cssTest">
+    async_test(t => {
+      requestAnimationFrame(t.step_func_done(_ => {
+        var script = document.querySelector('#cssTest');
+        var style = getComputedStyle(script);
+        assert_equals(style['display'], 'block');
+        assert_equals(style['background-image'], "url(\"http://web-platform.test:8001/security/resources/abe.png\")");
+      }));
+    }, "Nonces leak via CSS side-channels.");
+</script>
diff --git a/content-security-policy/_unapproved/script-nonces-hidden.html b/content-security-policy/_unapproved/script-nonces-hidden.html
new file mode 100644
index 00000000000000..41636331bbf82b
--- /dev/null
+++ b/content-security-policy/_unapproved/script-nonces-hidden.html
@@ -0,0 +1,114 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js" nonce="abc"></script>
+<script src="/resources/testharnessreport.js" nonce="abc"></script>
+
+<!-- `Content-Security-Policy: script-src 'nonce-abc'; img-src 'none'` delivered via headers -->
+
+<body>
+<!-- Basics -->
+<script nonce="abc" id="testScript">
+  document.currentScript.setAttribute('executed', 'yay');
+</script>
+
+<script nonce="abc">
+    var script = document.querySelector('#testScript');
+
+    test(t => {
+      // Query Selector
+      assert_equals(document.querySelector('body [nonce]'), script);
+      assert_equals(document.querySelector('body [nonce=""]'), script);
+      assert_equals(document.querySelector('body [nonce=abc]'), null);
+
+      assert_equals(script.getAttribute('nonce'), '');
+      assert_equals(script.nonce, 'abc');
+    }, "Reading 'nonce' content attribute and IDL attribute.");
+
+    // Clone node.
+    test(t => {
+      script.setAttribute('executed', 'boo');
+      var s2 = script.cloneNode();
+      assert_equals(s2.nonce, 'abc', 'IDL attribute');
+      assert_equals(s2.getAttribute('nonce'), '');
+    }, "Cloned node retains nonce.");
+
+    async_test(t => {
+      var s2 = script.cloneNode();
+      document.head.appendChild(s2);
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s2.nonce, 'abc');
+        assert_equals(s2.getAttribute('nonce'), '');
+
+        // The cloned script won't execute, as its 'already started' flag is set.
+        assert_equals(s2.getAttribute('executed'), 'boo');
+      }));
+    }, "Cloned node retains nonce when inserted.");
+
+    // Set the content attribute to 'foo'
+    test(t => {
+      script.setAttribute('nonce', 'foo');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+      assert_equals(script.nonce, 'abc');
+    }, "Writing 'nonce' content attribute.");
+
+    // Set the IDL attribute to 'bar'
+    test(t => {
+      script.nonce = 'bar';
+      assert_equals(script.nonce, 'bar');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+    }, "Writing 'nonce' IDL attribute.");
+
+    // Fragment parser.
+    var documentWriteTest = async_test("Document-written script executes.");
+    document.write(`<script nonce='abc'>
+      documentWriteTest.done();
+      test(t => {
+        var script = document.currentScript;
+        assert_equals(script.getAttribute('nonce'), '');
+        assert_equals(script.nonce, 'abc');
+      }, "Document-written script's nonce value.");
+    </scr` + `ipt>`);
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('script');
+      s.innerText = script.innerText;
+      s.nonce = 'abc';
+      document.head.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s.nonce, 'abc');
+        assert_equals(s.getAttribute('nonce'), null);
+      }));
+    }, "createElement.nonce.");
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('script');
+      s.innerText = script.innerText;
+      s.setAttribute('nonce', 'abc');
+      assert_equals(s.getAttribute('nonce'), 'abc', "Pre-insertion content");
+      assert_equals(s.nonce, '', "Pre-insertion IDL");
+      document.head.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s.nonce, 'abc', "Post-insertion IDL");
+        assert_equals(s.getAttribute('nonce'), '', "Post-insertion content");
+      }));
+    }, "createElement.setAttribute.");
+</script>
+
+<!-- CSS Leakage -->
+<style>
+    #cssTest { display: block; }
+    #cssTest[nonce=abc] { background: url(/security/resources/abe.png); }
+</style>
+<script nonce="abc" id="cssTest">
+    async_test(t => {
+      requestAnimationFrame(t.step_func_done(_ => {
+        var script = document.querySelector('#cssTest');
+        var style = getComputedStyle(script);
+        assert_equals(style['display'], 'block');
+        assert_equals(style['background-image'], 'none');
+      }));
+    }, "Nonces don't leak via CSS side-channels.");
+</script>
diff --git a/content-security-policy/_unapproved/script-nonces-hidden.html.headers b/content-security-policy/_unapproved/script-nonces-hidden.html.headers
new file mode 100644
index 00000000000000..ad8d0b54f31d6d
--- /dev/null
+++ b/content-security-policy/_unapproved/script-nonces-hidden.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'nonce-abc'; img-src 'none'
diff --git a/content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html b/content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html
new file mode 100644
index 00000000000000..46da267a94f01f
--- /dev/null
+++ b/content-security-policy/_unapproved/svgscript-nonces-hidden-meta.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<meta http-equiv="content-security-policy" content="script-src 'nonce-abc'; img-src 'none'">
+
+<body>
+<!-- Basics -->
+<svg xmlns="http://www.w3.org/2000/svg">
+  <script nonce="abc" id="testScript">
+    document.currentScript.setAttribute('executed', 'yay');
+  </script>
+</svg>
+
+<script nonce="abc">
+    var script = document.querySelector('#testScript');
+
+    test(t => {
+      // Query Selector
+      assert_equals(document.querySelector('[nonce]'), script);
+      assert_equals(document.querySelector('[nonce=""]'), null);
+      assert_equals(document.querySelector('[nonce=abc]'), script);
+
+      assert_equals(script.getAttribute('nonce'), 'abc');
+      assert_equals(script.nonce, 'abc');
+    }, "Reading 'nonce' content attribute and IDL attribute.");
+
+    // Clone node.
+    test(t => {
+      script.setAttribute('executed', 'boo');
+      var s2 = script.cloneNode();
+      assert_equals(s2.nonce, 'abc', 'IDL attribute');
+      assert_equals(s2.getAttribute('nonce'), 'abc');
+    }, "Cloned node retains nonce.");
+
+    async_test(t => {
+      var s2 = script.cloneNode();
+      document.head.appendChild(s2);
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s2.nonce, 'abc');
+        assert_equals(s2.getAttribute('nonce'), 'abc');
+
+        // The cloned script won't execute, as its 'already started' flag is set.
+        assert_equals(s2.getAttribute('executed'), 'boo');
+      }));
+    }, "Cloned node retains nonce when inserted.");
+
+    // Set the content attribute to 'foo'
+    test(t => {
+      script.setAttribute('nonce', 'foo');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+      assert_equals(script.nonce, 'abc');
+    }, "Writing 'nonce' content attribute.");
+
+    // Set the IDL attribute to 'bar'
+    test(t => {
+      script.nonce = 'bar';
+      assert_equals(script.nonce, 'bar');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+    }, "Writing 'nonce' IDL attribute.");
+
+    // Fragment parser.
+    var documentWriteTest = async_test("Document-written script executes.");
+    document.write(`<svg xmlns="http://www.w3.org/2000/svg"><script nonce='abc'>
+      documentWriteTest.done();
+      test(t => {
+        var script = document.currentScript;
+        assert_equals(script.getAttribute('nonce'), 'abc');
+        assert_equals(script.nonce, 'abc');
+      }, "Document-written script's nonce value.");
+    </scr` + `ipt></svg>`);
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('svg');
+      var innerScript = document.createElement('innerScript');
+      innerScript.innerText = script.innerText;
+      innerScript.nonce = 'abc';
+      s.appendChild(innerScript);
+      document.body.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(innerScript.nonce, 'abc');
+        assert_equals(innerScript.getAttribute('nonce'), null, 'innerScript.getAttribute nonce');
+      }));
+    }, "createElement.nonce.");
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('svg');
+      var innerScript = document.createElement('script');
+      innerScript.innerText = script.innerText;
+      innerScript.setAttribute('nonce', 'abc');
+      assert_equals(innerScript.getAttribute('nonce'), 'abc', "Pre-insertion content");
+      assert_equals(innerScript.nonce, '', "Pre-insertion IDL");
+      s.appendChild(innerScript);
+      document.body.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(innerScript.nonce, 'abc', "Post-insertion IDL");
+        assert_equals(innerScript.getAttribute('nonce'), 'abc', "Post-insertion content");
+      }));
+    }, "createElement.setAttribute.");
+</script>
+
+<!-- CSS Leakage -->
+<style>
+    #cssTest { display: block; }
+    #cssTest[nonce=abc] { background: url(/security/resources/abe.png); }
+</style>
+<svg xmlns="http://www.w3.org/2000/svg">
+  <script nonce="abc" id="cssTest">
+      async_test(t => {
+        requestAnimationFrame(t.step_func_done(_ => {
+          var script = document.querySelector('#cssTest');
+          var style = getComputedStyle(script);
+          assert_equals(style['display'], 'block');
+          assert_equals(style['background-image'], "url(\"http://web-platform.test:8001/security/resources/abe.png\")");
+        }));
+      }, "Nonces don't leak via CSS side-channels.");
+  </script>
+</svg>
diff --git a/content-security-policy/_unapproved/svgscript-nonces-hidden.html b/content-security-policy/_unapproved/svgscript-nonces-hidden.html
new file mode 100644
index 00000000000000..ddba1a9e8955a7
--- /dev/null
+++ b/content-security-policy/_unapproved/svgscript-nonces-hidden.html
@@ -0,0 +1,122 @@
+<!DOCTYPE html>
+<script src="/resources/testharness.js" nonce="abc"></script>
+<script src="/resources/testharnessreport.js" nonce="abc"></script>
+
+<!-- `Content-Security-Policy: script-src 'nonce-abc'; img-src 'none'` delivered via headers -->
+
+<body>
+<!-- Basics -->
+<svg xmlns="http://www.w3.org/2000/svg">
+  <script nonce="abc" id="testScript">
+    document.currentScript.setAttribute('executed', 'yay');
+  </script>
+</svg>
+
+<script nonce="abc">
+    var script = document.querySelector('#testScript');
+
+    test(t => {
+      // Query Selector
+      assert_equals(document.querySelector('body [nonce]'), script);
+      assert_equals(document.querySelector('body [nonce=""]'), script);
+      assert_equals(document.querySelector('body [nonce=abc]'), null);
+
+      assert_equals(script.getAttribute('nonce'), '');
+      assert_equals(script.nonce, 'abc');
+    }, "Reading 'nonce' content attribute and IDL attribute.");
+
+    // Clone node.
+    test(t => {
+      script.setAttribute('executed', 'boo');
+      var s2 = script.cloneNode();
+      assert_equals(s2.nonce, 'abc', 'IDL attribute');
+      assert_equals(s2.getAttribute('nonce'), '');
+    }, "Cloned node retains nonce.");
+
+    async_test(t => {
+      var s2 = script.cloneNode();
+      document.head.appendChild(s2);
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(s2.nonce, 'abc');
+        assert_equals(s2.getAttribute('nonce'), '');
+
+        // The cloned script won't execute, as its 'already started' flag is set.
+        assert_equals(s2.getAttribute('executed'), 'boo');
+      }));
+    }, "Cloned node retains nonce when inserted.");
+
+    // Set the content attribute to 'foo'
+    test(t => {
+      script.setAttribute('nonce', 'foo');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+      assert_equals(script.nonce, 'abc');
+    }, "Writing 'nonce' content attribute.");
+
+    // Set the IDL attribute to 'bar'
+    test(t => {
+      script.nonce = 'bar';
+      assert_equals(script.nonce, 'bar');
+      assert_equals(script.getAttribute('nonce'), 'foo');
+    }, "Writing 'nonce' IDL attribute.");
+
+    // Fragment parser.
+    var documentWriteTest = async_test("Document-written script executes.");
+    document.write(`<svg xmlns="http://www.w3.org/2000/svg"><script nonce='abc'>
+      documentWriteTest.done();
+      test(t => {
+        var script = document.currentScript;
+        assert_equals(script.getAttribute('nonce'), '');
+        assert_equals(script.nonce, 'abc');
+      }, "Document-written script's nonce value.");
+    </scr` + `ipt></svg>`);
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('svg');
+      var innerScript = document.createElement('script');
+      innerScript.innerText = script.innerText;
+      innerScript.nonce = 'abc';
+      s.appendChild(innerScript);
+      document.body.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(innerScript.nonce, 'abc');
+        assert_equals(innerScript.getAttribute('nonce'), null);
+      }));
+    }, "createElement.nonce.");
+
+    // Create node.
+    async_test(t => {
+      var s = document.createElement('svg');
+      var innerScript = document.createElement('script');
+      innerScript.innerText = script.innerText;
+      innerScript.setAttribute('nonce', 'abc');
+      assert_equals(innerScript.getAttribute('nonce'), 'abc', "Pre-insertion content");
+      assert_equals(innerScript.nonce, '', "Pre-insertion IDL");
+      s.appendChild(innerScript);
+      document.body.appendChild(s);
+
+      window.addEventListener('load', t.step_func_done(_ => {
+        assert_equals(innerScript.nonce, 'abc', "Post-insertion IDL");
+        assert_equals(innerScript.getAttribute('nonce'), '', "Post-insertion content");
+      }));
+    }, "createElement.setAttribute.");
+</script>
+
+<!-- CSS Leakage -->
+<style>
+    #cssTest { display: block; }
+    #cssTest[nonce=abc] { background: url(/security/resources/abe.png); }
+</style>
+<svg xmlns="http://www.w3.org/2000/svg">
+  <script nonce="abc" id="cssTest">
+      async_test(t => {
+        requestAnimationFrame(t.step_func_done(_ => {
+          var script = document.querySelector('#cssTest');
+          var style = getComputedStyle(script);
+          assert_equals(style['display'], 'block');
+          assert_equals(style['background-image'], 'none');
+        }));
+      }, "Nonces don't leak via CSS side-channels.");
+  </script>
+</svg>
diff --git a/content-security-policy/_unapproved/svgscript-nonces-hidden.html.headers b/content-security-policy/_unapproved/svgscript-nonces-hidden.html.headers
new file mode 100644
index 00000000000000..ad8d0b54f31d6d
--- /dev/null
+++ b/content-security-policy/_unapproved/svgscript-nonces-hidden.html.headers
@@ -0,0 +1 @@
+Content-Security-Policy: script-src 'nonce-abc'; img-src 'none'