summaryrefslogtreecommitdiffstats
path: root/toolkit/components/extensions/test/xpcshell/test_ext_csp_frame_ancestors.js
blob: ae931dfe06579dcca09f390acf1d319fd7e63157 (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
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

const server = createHttpServer({ hosts: ["example.com", "example.net"] });
server.registerPathHandler("/parent.html", (request, response) => {
  let frameUrl = new URLSearchParams(request.queryString).get("iframe_src");
  response.setHeader("Content-Type", "text/html; charset=utf-8", false);
  response.write(`<!DOCTYPE html><iframe src="${frameUrl}"></iframe>`);
});

// Loads an extension frame as a frame at ancestorOrigins[0], which in turn is
// a child of ancestorOrigins[1], etc.
// The frame should either load successfully, or trigger exactly one failure due
// to one of the ancestorOrigins being blocked by the content_security_policy.
async function checkExtensionLoadInFrame({
  ancestorOrigins,
  content_security_policy,
  expectLoad,
}) {
  const extensionData = {
    manifest: {
      content_security_policy,
      web_accessible_resources: ["parent.html", "frame.html"],
    },
    files: {
      "frame.html": `<!DOCTYPE html><script src="frame.js"></script>`,
      "frame.js": () => {
        browser.test.sendMessage("frame_load_completed");
      },
      "parent.html": `<!DOCTYPE html><body><script src="parent.js"></script>`,
      "parent.js": () => {
        let iframe = document.createElement("iframe");
        iframe.src = new URLSearchParams(location.search).get("iframe_src");
        document.body.append(iframe);
      },
    },
  };
  let extension = ExtensionTestUtils.loadExtension(extensionData);
  await extension.startup();

  const EXTENSION_FRAME_URL = `moz-extension://${extension.uuid}/frame.html`;

  // ancestorOrigins is a list of origins, from the parent up to the top frame.
  let topUrl = EXTENSION_FRAME_URL;
  for (let origin of ancestorOrigins) {
    if (origin === "EXTENSION_ORIGIN") {
      origin = `moz-extension://${extension.uuid}`;
    }
    // origin is either the origin for |server| or the test extension. Both
    // endpoints serve a page at parent.html that embeds iframe_src.
    topUrl = `${origin}/parent.html?iframe_src=${encodeURIComponent(topUrl)}`;
  }

  let cspViolationObserver;
  let cspViolationCount = 0;
  let frameLoadedCount = 0;
  let frameLoadOrFailedPromise = new Promise(resolve => {
    extension.onMessage("frame_load_completed", () => {
      ++frameLoadedCount;
      resolve();
    });
    cspViolationObserver = {
      observe(subject, topic, data) {
        ++cspViolationCount;
        Assert.equal(data, "frame-ancestors", "CSP violation directive");
        resolve();
      },
    };
    Services.obs.addObserver(cspViolationObserver, "csp-on-violate-policy");
  });

  const contentPage = await ExtensionTestUtils.loadContentPage(topUrl);

  // Firstly, wait for the frame load to either complete or fail.
  await frameLoadOrFailedPromise;

  // Secondly, do a round trip to the content process to make sure that any
  // unexpected extra load/failures are observed. This is necessary, because
  // the "csp-on-violate-policy" notification is triggered from the parent,
  // while it may be possible for the load to continue in the child anyway.
  //
  // And while we are at it, this verifies that the CSP does not block regular
  // reads of a file that's part of web_accessible_resources. For comparable
  // results, the load should ideally happen in the parent of the extension
  // frame, but contentPage.fetch only works in the top frame, so this does not
  // work perfectly in case ancestorOrigins.length > 1.
  // But that is OK, as we mainly care about unexpected frame loads/failures.
  equal(
    await contentPage.fetch(EXTENSION_FRAME_URL),
    extensionData.files["frame.html"],
    "web-accessible extension resource can still be read with fetch"
  );

  // Finally, clean up.
  Services.obs.removeObserver(cspViolationObserver, "csp-on-violate-policy");
  await contentPage.close();
  await extension.unload();

  if (expectLoad) {
    equal(cspViolationCount, 0, "Expected no CSP violations");
    equal(
      frameLoadedCount,
      1,
      `Frame should accept ancestors (${ancestorOrigins}) in CSP: ${content_security_policy}`
    );
  } else {
    equal(cspViolationCount, 1, "Expected CSP violation count");
    equal(
      frameLoadedCount,
      0,
      `Frame should reject one of the ancestors (${ancestorOrigins}) in CSP: ${content_security_policy}`
    );
  }
}

add_task(async function test_frame_ancestors_missing_allows_self() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["EXTENSION_ORIGIN"],
    content_security_policy: "default-src 'self'", // missing frame-ancestors.
    expectLoad: true, // an extension can embed itself by default.
  });
});

add_task(async function test_frame_ancestors_self_allows_self() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["EXTENSION_ORIGIN"],
    content_security_policy: "default-src 'self'; frame-ancestors 'self'",
    expectLoad: true,
  });
});

add_task(async function test_frame_ancestors_none_blocks_self() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["EXTENSION_ORIGIN"],
    content_security_policy: "default-src 'self'; frame-ancestors",
    expectLoad: false, // frame-ancestors 'none' blocks extension frame.
  });
});

add_task(async function test_frame_ancestors_missing_allowed_in_web_page() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com"],
    content_security_policy: "default-src 'self'", // missing frame-ancestors
    expectLoad: true, // Web page can embed web-accessible extension frames.
  });
});

add_task(async function test_frame_ancestors_self_blocked_in_web_page() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com"],
    content_security_policy: "default-src 'self'; frame-ancestors 'self'",
    expectLoad: false,
  });
});

add_task(async function test_frame_ancestors_scheme_allowed_in_web_page() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com"],
    content_security_policy: "default-src 'self'; frame-ancestors http:",
    expectLoad: true,
  });
});

add_task(async function test_frame_ancestors_origin_allowed_in_web_page() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com"],
    content_security_policy:
      "default-src 'self'; frame-ancestors http://example.com",
    expectLoad: true,
  });
});

add_task(async function test_frame_ancestors_mismatch_blocked_in_web_page() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com"],
    content_security_policy:
      "default-src 'self'; frame-ancestors http://not.example.com",
    expectLoad: false,
  });
});

add_task(async function test_frame_ancestors_top_mismatch_blocked() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com", "http://example.net"],
    content_security_policy:
      "default-src 'self'; frame-ancestors http://example.com",
    // example.com is allowed, but the top origin (example.net) is rejected.
    expectLoad: false,
  });
});

add_task(async function test_frame_ancestors_parent_mismatch_blocked() {
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.net", "http://example.com"],
    content_security_policy:
      "default-src 'self'; frame-ancestors http://example.com",
    // example.com is allowed, but the parent origin (example.net) is rejected.
    expectLoad: false,
  });
});

add_task(async function test_frame_ancestors_middle_rejected() {
  if (!WebExtensionPolicy.useRemoteWebExtensions) {
    // This test load http://example.com in an extension page, which fails if
    // extensions run in the parent process. This is not a default config on
    // desktop, but see https://bugzilla.mozilla.org/show_bug.cgi?id=1724099
    info("Web pages cannot be loaded in extension page without OOP extensions");
    return;
  }
  await checkExtensionLoadInFrame({
    ancestorOrigins: ["http://example.com", "EXTENSION_ORIGIN"],
    content_security_policy:
      "default-src 'self'; frame-src http: 'self'; frame-ancestors 'self'",
    // Although the top frame has the same origin as the extension, the load
    // should be rejected anyway because there is a non-allowlisted origin in
    // the middle (child of top frame, parent of extension frame).
    expectLoad: false,
  });
});