summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/trusted-types/block-text-node-insertion-into-script-element.html
blob: 08e3e695530302c8875d127bc4d36e2085a1a1d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
<!DOCTYPE html>
<html>
<head>
  <script src="/resources/testharness.js"></script>
  <script src="/resources/testharnessreport.js"></script>
  <meta http-equiv="Content-Security-Policy" content="require-trusted-types-for 'script';">
</head>
<body>
<div id="container"></div>
<script id="script1">"hello world!";</script>
<script id="trigger"></script>
<script>
  var container = document.querySelector("#container");
  const policy_dict = {
    createScript: s => (s.includes("fail") ? null : s.replace("default", "count")),
    createHTML: s => s.replace(/2/g, "3"),
  };
  const policy = trustedTypes.createPolicy("policy", policy_dict);

  // Regression tests for 'Bypass via insertAdjacentText', reported at
  // https://github.com/w3c/trusted-types/issues/133

  // We are trying to assert that scripts do _not_ get executed. We
  // accomplish by having the script under examination containing a
  // postMessage, and to send a second guaranteed-to-execute postMessage
  // so there's a point in time when we're sure the first postMessage
  // must have arrived (if indeed it had been sent).
  //
  // We'll interpret the message data as follows:
  // - includes "block": error (this message should have been blocked by TT)
  // - includes "count": Count these, and later check against expect_count.
  // - includes "done": Unregister the event handler and finish the test.
  // - all else: Reject, as this is probably an error in the test.
  function checkMessage(expect_count) {
    postMessage("done", "*");
    return new Promise((resolve, reject) => {
      let count = 0;
      globalThis.addEventListener("message", function handler(e) {
        if (e.data.includes("block")) {
          reject(`'block' received (${e.data})`);
        } else if (e.data.includes("count")) {
          count = count + 1;
        } else if (e.data.includes("done")) {
          globalThis.removeEventListener("message", handler);
          if (expect_count && count != expect_count) {
            reject(
                `'done' received, but unexpected counts: expected ${expect_count} != actual ${count} (${e.data})`);
          } else {
            resolve(e.data);
          }
        } else {
          reject("unexpected message received: " + e.data);
        }
      });
    });
  }

  function checkSecurityPolicyViolationEvent(expect_count) {
    return new Promise((resolve, reject) => {
      let count = 0;
      document.addEventListener("securitypolicyviolation", e => {
        if (e.sample.includes("trigger")) {
          if (expect_count && count != expect_count) {
            reject(
                `'trigger' received, but unexpected counts: expected ${expect_count} != actual ${count}`);
          } else {
            resolve();
          }
        } else {
          count = count + 1;
        }
      });
      try {
        document.getElementById("trigger").text = "trigger fail";
      } catch(e) { }
    });
  }

  // Original report:
  promise_test(t => {
    // Setup: Create a <script> element with a <p> child.
    let s = document.createElement("script");

    // Sanity check: Element child content (<p>) doesn't count as source text.
    let p = document.createElement("p");
    p.text = "hello('world');";
    s.appendChild(p);
    container.appendChild(s);
    assert_equals(s.text, "");

    // Try to insertAdjacentText into the <script>, starting from the <p>
    p.insertAdjacentText("beforebegin",
                         "postMessage('beforebegin should be blocked', '*');");
    assert_true(s.text.includes("postMessage"));
    p.insertAdjacentText("afterend",
                         "postMessage('afterend should be blocked', '*');");
    assert_true(s.text.includes("after"));
    return checkMessage();
  }, "Regression test: Bypass via insertAdjacentText, initial comment.");

  const script_elements = {
    "script": doc => doc.createElement("script"),
    "svg:script": doc => doc.createElementNS("http://www.w3.org/2000/svg", "script"),
  };
  for (let [element, create_element] of Object.entries(script_elements)) {
    // Like the "Original Report" test case above, but avoids use of the "text"
    // accessor that <svg:script> doesn't support.
    promise_test(t => {
      let s = create_element(document);

      // Sanity check: Element child content (<p>) doesn't count as source text.
      let p = document.createElement("p");
      p.textContent = "hello('world');";
      s.appendChild(p);
      container.appendChild(s);

      // Try to insertAdjacentText into the <script>, starting from the <p>
      p.insertAdjacentText("beforebegin",
                           "postMessage('beforebegin should be blocked', '*');");
      assert_true(s.textContent.includes("postMessage"));
      p.insertAdjacentText("afterend",
                           "postMessage('afterend should be blocked', '*');");
      assert_true(s.textContent.includes("after"));
      return checkMessage();
    }, "Regression test: Bypass via insertAdjacentText, initial comment. " + element);

    // Variant: Create a <script> element and create & insert a text node. Then
    // insert it into the document container to make it live.
    promise_test(t => {
      // Setup: Create a <script> element, and insert text via a text node.
      let s = create_element(document);
      let text = document.createTextNode("postMessage('appendChild with a " +
                                         "text node should be blocked', '*');");
      s.appendChild(text);
      container.appendChild(s);
      return checkMessage();
    }, "Regression test: Bypass via appendChild into off-document script element" + element);

    // Variant: Create a <script> element and insert it into the document. Then
    // create a text node and insert it into the live <script> element.
    promise_test(t => {
      // Setup: Create a <script> element, insert it into the doc, and then create
      // and insert text via a text node.
      let s = create_element(document);
      container.appendChild(s);
      let text = document.createTextNode("postMessage('appendChild with a live " +
                                         "text node should be blocked', '*');");
      s.appendChild(text);
      return checkMessage();
    }, "Regression test: Bypass via appendChild into live script element " + element);
  }

  promise_test(t => {
    return checkSecurityPolicyViolationEvent();
  }, "Prep for subsequent tests: Clear SPV event queue.");

  promise_test(t => {
    const inserted_script = document.getElementById("script1");
    assert_throws_js(TypeError, _ => {
        inserted_script.innerHTML = "2+2";
    });

    let new_script = document.createElement("script");
    assert_throws_js(TypeError, _ => {
      new_script.innerHTML = "2+2";
      container.appendChild(new_script);
    });

    const trusted_html = policy.createHTML("2*4");
    assert_equals("" + trusted_html, "3*4");
    inserted_script.innerHTML = trusted_html;
    assert_equals(inserted_script.textContent, "3*4");

    new_script = document.createElement("script");
    new_script.innerHTML = trusted_html;
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3*4");

    // We expect 3 SPV events: two for the two assert_throws_js cases, and one
    // for script element, which will be rejected at the time of execution.
    return checkSecurityPolicyViolationEvent(3);
  }, "Spot tests around script + innerHTML interaction.");


  // Create default policy. Wrapped in a promise_test to ensure it runs only
  // after the other tests.
  let default_policy;
  promise_test(t => {
    default_policy = trustedTypes.createPolicy("default", policy_dict);
    return Promise.resolve();
  }, "Prep for subsequent tests: Create default policy.");

  for (let [element, create_element] of Object.entries(script_elements)) {
    promise_test(t => {
      let s = create_element(document);
      let text = document.createTextNode("postMessage('default', '*');");
      s.appendChild(text);
      container.appendChild(s);
      return Promise.all([checkMessage(1),
                            checkSecurityPolicyViolationEvent(0)]);
    }, "Test that default policy applies. " + element);

    promise_test(t => {
      let s = create_element(document);
      let text = document.createTextNode("fail");
      s.appendChild(text);
      container.appendChild(s);
      return Promise.all([checkMessage(0),
                          checkSecurityPolicyViolationEvent(1)]);
    }, "Test a failing default policy. " + element);
  }

  promise_test(t => {
    const inserted_script = document.getElementById("script1");
    inserted_script.innerHTML = "2+2";
    assert_equals(inserted_script.textContent, "3+3");

    let new_script = document.createElement("script");
    new_script.innerHTML = "2+2";
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3+3");

    const trusted_html = default_policy.createHTML("2*4");
    assert_equals("" + trusted_html, "3*4");
    inserted_script.innerHTML = trusted_html;
    assert_equals(inserted_script.textContent, "3*4");

    new_script = document.createElement("script");
    new_script.innerHTML = trusted_html;
    container.appendChild(new_script);
    assert_equals(inserted_script.textContent, "3*4");

    return checkSecurityPolicyViolationEvent(0);
  }, "Spot tests around script + innerHTML interaction with default policy.");
</script>
</body>
</html>