browser_webextension_inspected_window_access.js (10215B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 async function run_inspectedWindow_eval({ tab, codeToEval, extension }) { 7 const fakeExtCallerInfo = { 8 url: `moz-extension://${extension.uuid}/another/fake-caller-script.js`, 9 lineNumber: 1, 10 addonId: extension.id, 11 }; 12 const commands = await CommandsFactory.forTab(tab, { isWebExtension: true }); 13 await commands.targetCommand.startListening(); 14 const result = await commands.inspectedWindowCommand.eval( 15 fakeExtCallerInfo, 16 codeToEval, 17 {} 18 ); 19 await commands.destroy(); 20 return result; 21 } 22 23 async function openAboutBlankTabWithExtensionOrigin(extension) { 24 const tab = await BrowserTestUtils.openNewForegroundTab( 25 gBrowser, 26 `moz-extension://${extension.uuid}/manifest.json` 27 ); 28 const loaded = BrowserTestUtils.browserLoaded(tab.linkedBrowser, { 29 wantLoad: "about:blank", 30 }); 31 await ContentTask.spawn(tab.linkedBrowser, null, () => { 32 // about:blank inherits the principal when opened from content. 33 content.wrappedJSObject.location.assign("about:blank"); 34 }); 35 await loaded; 36 // Sanity checks: 37 is(tab.linkedBrowser.currentURI.spec, "about:blank", "expected tab"); 38 is( 39 tab.linkedBrowser.contentPrincipal.originNoSuffix, 40 `moz-extension://${extension.uuid}`, 41 "about:blank should be at the extension origin" 42 ); 43 return tab; 44 } 45 46 async function checkEvalResult({ 47 extension, 48 description, 49 url, 50 createTab = () => BrowserTestUtils.openNewForegroundTab(gBrowser, url), 51 expectedResult, 52 }) { 53 const tab = await createTab(); 54 is(tab.linkedBrowser.currentURI.spec, url, "Sanity check: tab URL"); 55 const result = await run_inspectedWindow_eval({ 56 tab, 57 codeToEval: "'code executed at ' + location.href", 58 extension, 59 }); 60 BrowserTestUtils.removeTab(tab); 61 SimpleTest.isDeeply( 62 result, 63 expectedResult, 64 `eval result for devtools.inspectedWindow.eval at ${url} (${description})` 65 ); 66 } 67 68 async function checkEvalAllowed({ extension, description, url, createTab }) { 69 info(`checkEvalAllowed: ${description} (at URL: ${url})`); 70 await checkEvalResult({ 71 extension, 72 description, 73 url, 74 createTab, 75 expectedResult: { value: `code executed at ${url}` }, 76 }); 77 } 78 async function checkEvalDenied({ extension, description, url, createTab }) { 79 info(`checkEvalDenied: ${description} (at URL: ${url})`); 80 await checkEvalResult({ 81 extension, 82 description, 83 url, 84 createTab, 85 expectedResult: { 86 exceptionInfo: { 87 isError: true, 88 code: "E_PROTOCOLERROR", 89 details: [ 90 "This extension is not allowed on the current inspected window origin", 91 ], 92 description: "Inspector protocol error: %s", 93 }, 94 }, 95 }); 96 } 97 98 add_task(async function test_eval_at_http() { 99 await SpecialPowers.pushPrefEnv({ 100 set: [["dom.security.https_first", false]], 101 }); 102 103 // eslint-disable-next-line @microsoft/sdl/no-insecure-url 104 const httpUrl = "http://example.com/"; 105 106 // When running with --use-http3-server, http:-URLs cannot be loaded. 107 try { 108 await fetch(httpUrl); 109 } catch { 110 info("Skipping test_eval_at_http because http:-URL cannot be loaded"); 111 return; 112 } 113 114 const extension = ExtensionTestUtils.loadExtension({}); 115 await extension.startup(); 116 117 await checkEvalAllowed({ 118 extension, 119 description: "http:-URL", 120 url: httpUrl, 121 }); 122 await extension.unload(); 123 124 await SpecialPowers.popPrefEnv(); 125 }); 126 127 add_task(async function test_eval_at_https() { 128 const extension = ExtensionTestUtils.loadExtension({}); 129 await extension.startup(); 130 131 const privilegedExtension = ExtensionTestUtils.loadExtension({ 132 isPrivileged: true, 133 }); 134 await privilegedExtension.startup(); 135 136 await checkEvalAllowed({ 137 extension, 138 description: "https:-URL", 139 url: "https://example.com/", 140 }); 141 142 await checkEvalAllowed({ 143 extension, 144 description: "https:-URL with opaque origin (CSP sandbox)", 145 url: URL_ROOT_SSL + "csp_sandbox.sjs", 146 }); 147 148 await checkEvalDenied({ 149 extension, 150 description: "a restricted domain", 151 // Domain in extensions.webextensions.restrictedDomains by browser.toml. 152 url: "https://test2.example.com/", 153 }); 154 155 await SpecialPowers.pushPrefEnv({ 156 set: [["extensions.quarantinedDomains.list", "example.com"]], 157 }); 158 159 await checkEvalDenied({ 160 extension, 161 description: "a quarantined domain", 162 url: "https://example.com/", 163 }); 164 165 await checkEvalAllowed({ 166 extension: privilegedExtension, 167 description: "a quarantined domain", 168 url: "https://example.com/", 169 }); 170 171 await SpecialPowers.popPrefEnv(); 172 173 await extension.unload(); 174 await privilegedExtension.unload(); 175 }); 176 177 add_task(async function test_eval_at_sandboxed_page() { 178 const extension = ExtensionTestUtils.loadExtension({}); 179 await extension.startup(); 180 181 await checkEvalAllowed({ 182 extension, 183 description: "page with CSP sandbox", 184 url: "https://example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x", 185 }); 186 await checkEvalDenied({ 187 extension, 188 description: "restricted domain with CSP sandbox", 189 url: "https://test2.example.com/document-builder.sjs?headers=Content-Security-Policy:sandbox&html=x", 190 }); 191 192 await extension.unload(); 193 }); 194 195 add_task(async function test_eval_at_own_extension_origin_allowed() { 196 const extension = ExtensionTestUtils.loadExtension({ 197 background() { 198 // eslint-disable-next-line no-undef 199 browser.test.sendMessage( 200 "blob_url", 201 URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }])) 202 ); 203 }, 204 files: { 205 "mozext.html": `<!DOCTYPE html>moz-extension: here`, 206 }, 207 }); 208 await extension.startup(); 209 const blobUrl = await extension.awaitMessage("blob_url"); 210 211 await checkEvalAllowed({ 212 extension, 213 description: "moz-extension:-URL from own extension", 214 url: `moz-extension://${extension.uuid}/mozext.html`, 215 }); 216 await checkEvalAllowed({ 217 extension, 218 description: "blob:-URL from own extension", 219 url: blobUrl, 220 }); 221 await checkEvalAllowed({ 222 extension, 223 description: "about:blank with origin from own extension", 224 url: "about:blank", 225 createTab: () => openAboutBlankTabWithExtensionOrigin(extension), 226 }); 227 228 await extension.unload(); 229 }); 230 231 add_task(async function test_eval_at_other_extension_denied() { 232 // The extension for which we simulate devtools_page, chosen as caller of 233 // devtools.inspectedWindow.eval API calls. 234 const extension = ExtensionTestUtils.loadExtension({}); 235 await extension.startup(); 236 237 // The other extension, that |extension| should not be able to access: 238 const otherExt = ExtensionTestUtils.loadExtension({ 239 background() { 240 // eslint-disable-next-line no-undef 241 browser.test.sendMessage( 242 "blob_url", 243 URL.createObjectURL(new Blob(["blob: here", { type: "text/html" }])) 244 ); 245 }, 246 files: { 247 "mozext.html": `<!DOCTYPE html>moz-extension: here`, 248 }, 249 }); 250 await otherExt.startup(); 251 const otherExtBlobUrl = await otherExt.awaitMessage("blob_url"); 252 253 await checkEvalDenied({ 254 extension, 255 description: "moz-extension:-URL from another extension", 256 url: `moz-extension://${otherExt.uuid}/mozext.html`, 257 }); 258 await checkEvalDenied({ 259 extension, 260 description: "blob:-URL from another extension", 261 url: otherExtBlobUrl, 262 }); 263 await checkEvalDenied({ 264 extension, 265 description: "about:blank with origin from another extension", 266 url: "about:blank", 267 createTab: () => openAboutBlankTabWithExtensionOrigin(otherExt), 268 }); 269 270 await otherExt.unload(); 271 await extension.unload(); 272 }); 273 274 add_task(async function test_eval_at_about() { 275 const extension = ExtensionTestUtils.loadExtension({}); 276 await extension.startup(); 277 await checkEvalAllowed({ 278 extension, 279 description: "about:blank (null principal)", 280 url: "about:blank", 281 }); 282 await checkEvalDenied({ 283 extension, 284 description: "about:addons (system principal)", 285 url: "about:addons", 286 }); 287 await checkEvalDenied({ 288 extension, 289 description: "about:robots (about page)", 290 url: "about:robots", 291 }); 292 await extension.unload(); 293 }); 294 295 add_task(async function test_eval_at_file() { 296 // FYI: There is also an equivalent test case with a full end-to-end test at: 297 // browser/components/extensions/test/browser/browser_ext_devtools_inspectedWindow_eval_file.js 298 299 const extension = ExtensionTestUtils.loadExtension({}); 300 await extension.startup(); 301 302 // A dummy file URL that can be loaded in a tab. 303 const fileUrl = 304 "file://" + 305 getTestFilePath("browser_webextension_inspected_window_access.js"); 306 307 // checkEvalAllowed test helper cannot be used, because the file:-URL may 308 // redirect elsewhere, so the comparison with the full URL fails. 309 const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, fileUrl); 310 const result = await run_inspectedWindow_eval({ 311 tab, 312 codeToEval: "'code executed at ' + location.protocol", 313 extension, 314 }); 315 BrowserTestUtils.removeTab(tab); 316 SimpleTest.isDeeply( 317 result, 318 { value: "code executed at file:" }, 319 `eval result for devtools.inspectedWindow.eval at ${fileUrl}` 320 ); 321 322 await extension.unload(); 323 }); 324 325 add_task(async function test_eval_at_view_source() { 326 const extension = ExtensionTestUtils.loadExtension({}); 327 await extension.startup(); 328 const otherExt = ExtensionTestUtils.loadExtension({}); 329 await otherExt.startup(); 330 331 await checkEvalDenied({ 332 extension, 333 description: "view-source at https:-URL", 334 url: "view-source:https://example.com/?somepage", 335 }); 336 await checkEvalDenied({ 337 extension, 338 description: "view-source at https:-URL with opaque origin (CSP sandbox)", 339 url: `view-source:${URL_ROOT_SSL}csp_sandbox.sjs`, 340 }); 341 await checkEvalDenied({ 342 extension, 343 description: "view-source from own extension", 344 url: `view-source:moz-extension://${extension.uuid}/manifest.json?`, 345 }); 346 await checkEvalDenied({ 347 extension, 348 description: "view-source from another extension", 349 url: `view-source:moz-extension://${otherExt.uuid}/manifest.json?`, 350 }); 351 352 await otherExt.unload(); 353 await extension.unload(); 354 });