browser_content_sandbox_fs_tests.js (19721B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 /* import-globals-from browser_content_sandbox_utils.js */ 4 "use strict"; 5 6 const lazy = {}; 7 8 /* getLibcConstants is only present on *nix */ 9 ChromeUtils.defineLazyGetter(lazy, "LIBC", () => 10 ChromeUtils.getLibcConstants() 11 ); 12 13 // Test if the content process can create in $HOME, this should fail 14 async function createFileInHome() { 15 let browser = gBrowser.selectedBrowser; 16 let homeFile = fileInHomeDir(); 17 let path = homeFile.path; 18 let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); 19 ok(!fileCreated.ok, "creating a file in home dir failed"); 20 is( 21 fileCreated.code, 22 Cr.NS_ERROR_FILE_ACCESS_DENIED, 23 "creating a file in home dir failed with access denied" 24 ); 25 if (fileCreated.ok) { 26 // content process successfully created the file, now remove it 27 homeFile.remove(false); 28 } 29 } 30 31 // Test if the content process can create a temp file, this is forbidden on all 32 // platforms. Also test that the content process cannot create symlinks on 33 // macOS/Linux or delete files. 34 async function createTempFile() { 35 // On Windows we allow access to the temp dir for DEBUG builds, because of 36 // logging that uses that dir. 37 let isDbgWin = isWin() && SpecialPowers.isDebugBuild; 38 39 let browser = gBrowser.selectedBrowser; 40 let path = fileInTempDir().path; 41 let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); 42 if (isDbgWin) { 43 ok(fileCreated.ok, "creating a file in temp suceeded"); 44 } else { 45 ok(!fileCreated.ok, "creating a file in temp failed"); 46 is( 47 fileCreated.code, 48 Cr.NS_ERROR_FILE_ACCESS_DENIED, 49 "creating a file in temp failed with access denied" 50 ); 51 } 52 53 // now delete the file 54 let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile); 55 if (isDbgWin) { 56 ok(fileDeleted.ok, "deleting a file in temp succeeded"); 57 } else { 58 ok(!fileDeleted.ok, "deleting a file in temp failed"); 59 const expectedError = isLinux() 60 ? Cr.NS_ERROR_FILE_ACCESS_DENIED 61 : Cr.NS_ERROR_FILE_NOT_FOUND; 62 is( 63 fileDeleted.code, 64 expectedError, 65 "deleting a file in temp failed with access denied" 66 ); 67 } 68 69 // Test that symlink creation is not allowed on macOS/Linux. 70 if (isMac() || isLinux()) { 71 let path = fileInTempDir().path; 72 let symlinkCreated = await SpecialPowers.spawn( 73 browser, 74 [path], 75 createSymlink 76 ); 77 ok(!symlinkCreated.ok, "created a symlink in temp failed"); 78 const expectedError = isLinux() ? lazy.LIBC.EACCES : lazy.LIBC.EPERM; 79 is( 80 symlinkCreated.code, 81 expectedError, 82 "created a symlink in temp failed with access denied" 83 ); 84 } 85 } 86 87 // Test reading files and dirs from web and file content processes. 88 async function testFileAccessAllPlatforms() { 89 let webBrowser = GetWebBrowser(); 90 let fileContentProcessEnabled = isFileContentProcessEnabled(); 91 let fileBrowser = GetFileBrowser(); 92 93 // Directories/files to test accessing from content processes. 94 // For directories, we test whether a directory listing is allowed 95 // or blocked. For files, we test if we can read from the file. 96 // Each entry in the array represents a test file or directory 97 // that will be read from either a web or file process. 98 let tests = []; 99 100 let profileDir = GetProfileDir(); 101 tests.push({ 102 desc: "profile dir", // description 103 ok: false, // expected to succeed? 104 browser: webBrowser, // browser to run test in 105 file: profileDir, // nsIFile object 106 minLevel: minProfileReadSandboxLevel(), // min level to enable test 107 func: readDir, 108 }); 109 if (fileContentProcessEnabled) { 110 tests.push({ 111 desc: "profile dir", 112 ok: true, 113 browser: fileBrowser, 114 file: profileDir, 115 minLevel: 0, 116 func: readDir, 117 }); 118 } 119 120 let homeDir = GetHomeDir(); 121 tests.push({ 122 desc: "home dir", 123 ok: false, 124 browser: webBrowser, 125 file: homeDir, 126 minLevel: minHomeReadSandboxLevel(), 127 func: readDir, 128 }); 129 if (fileContentProcessEnabled) { 130 tests.push({ 131 desc: "home dir", 132 ok: true, 133 browser: fileBrowser, 134 file: homeDir, 135 minLevel: 0, 136 func: readDir, 137 }); 138 } 139 140 let extensionsDir = GetProfileEntry("extensions"); 141 if (extensionsDir.exists() && extensionsDir.isDirectory()) { 142 tests.push({ 143 desc: "extensions dir", 144 ok: true, 145 browser: webBrowser, 146 file: extensionsDir, 147 minLevel: 0, 148 func: readDir, 149 }); 150 } else { 151 ok(false, `${extensionsDir.path} is a valid dir`); 152 } 153 154 let chromeDir = GetProfileEntry("chrome"); 155 if (chromeDir.exists() && chromeDir.isDirectory()) { 156 tests.push({ 157 desc: "chrome dir", 158 ok: true, 159 browser: webBrowser, 160 file: chromeDir, 161 minLevel: 0, 162 func: readDir, 163 }); 164 } else { 165 ok(false, `${chromeDir.path} is valid dir`); 166 } 167 168 let cookiesFile = GetProfileEntry("cookies.sqlite"); 169 if (cookiesFile.exists() && !cookiesFile.isDirectory()) { 170 tests.push({ 171 desc: "cookies file", 172 ok: false, 173 browser: webBrowser, 174 file: cookiesFile, 175 minLevel: minProfileReadSandboxLevel(), 176 func: readFile, 177 }); 178 if (fileContentProcessEnabled) { 179 tests.push({ 180 desc: "cookies file", 181 ok: true, 182 browser: fileBrowser, 183 file: cookiesFile, 184 minLevel: 0, 185 func: readFile, 186 }); 187 } 188 } else { 189 ok(false, `${cookiesFile.path} is a valid file`); 190 } 191 192 if (isMac() || isLinux()) { 193 let varDir = GetDir("/var"); 194 195 if (isMac()) { 196 // Mac sandbox rules use /private/var because /var is a symlink 197 // to /private/var on OS X. Make sure that hasn't changed. 198 varDir.normalize(); 199 Assert.strictEqual( 200 varDir.path, 201 "/private/var", 202 "/var resolves to /private/var" 203 ); 204 } 205 206 tests.push({ 207 desc: "/var", 208 ok: false, 209 browser: webBrowser, 210 file: varDir, 211 minLevel: minHomeReadSandboxLevel(), 212 func: readDir, 213 }); 214 if (fileContentProcessEnabled) { 215 tests.push({ 216 desc: "/var", 217 ok: true, 218 browser: fileBrowser, 219 file: varDir, 220 minLevel: 0, 221 func: readDir, 222 }); 223 } 224 } 225 226 await runTestsList(tests); 227 } 228 229 async function testFileAccessMacOnly() { 230 if (!isMac()) { 231 return; 232 } 233 234 let webBrowser = GetWebBrowser(); 235 let fileContentProcessEnabled = isFileContentProcessEnabled(); 236 let fileBrowser = GetFileBrowser(); 237 let level = GetSandboxLevel(); 238 239 let tests = []; 240 241 // If ~/Library/Caches/TemporaryItems exists, when level <= 2 we 242 // make sure it's readable. For level 3, we make sure it isn't. 243 let homeTempDir = GetHomeDir(); 244 homeTempDir.appendRelativePath("Library/Caches/TemporaryItems"); 245 if (homeTempDir.exists()) { 246 let shouldBeReadable, minLevel; 247 if (level >= minHomeReadSandboxLevel()) { 248 shouldBeReadable = false; 249 minLevel = minHomeReadSandboxLevel(); 250 } else { 251 shouldBeReadable = true; 252 minLevel = 0; 253 } 254 tests.push({ 255 desc: "home library cache temp dir", 256 ok: shouldBeReadable, 257 browser: webBrowser, 258 file: homeTempDir, 259 minLevel, 260 func: readDir, 261 }); 262 } 263 264 // Test if we can read from $TMPDIR because we expect it 265 // to be within /private/var. Reading from it should be 266 // prevented in a 'web' process. 267 let macTempDir = GetDirFromEnvVariable("TMPDIR"); 268 269 macTempDir.normalize(); 270 Assert.ok( 271 macTempDir.path.startsWith("/private/var"), 272 "$TMPDIR is in /private/var" 273 ); 274 275 tests.push({ 276 desc: `$TMPDIR (${macTempDir.path})`, 277 ok: false, 278 browser: webBrowser, 279 file: macTempDir, 280 minLevel: minHomeReadSandboxLevel(), 281 func: readDir, 282 }); 283 if (fileContentProcessEnabled) { 284 tests.push({ 285 desc: `$TMPDIR (${macTempDir.path})`, 286 ok: true, 287 browser: fileBrowser, 288 file: macTempDir, 289 minLevel: 0, 290 func: readDir, 291 }); 292 } 293 294 // The font registry directory is in the Darwin user cache dir which is 295 // accessible with the getconf(1) library call using DARWIN_USER_CACHE_DIR. 296 // For this test, assume the cache dir is located at $TMPDIR/../C and use 297 // the $TMPDIR to derive the path to the registry. 298 let fontRegistryDir = macTempDir.parent.clone(); 299 fontRegistryDir.appendRelativePath("C/com.apple.FontRegistry"); 300 if (fontRegistryDir.exists()) { 301 tests.push({ 302 desc: `FontRegistry (${fontRegistryDir.path})`, 303 ok: true, 304 browser: webBrowser, 305 file: fontRegistryDir, 306 minLevel: minHomeReadSandboxLevel(), 307 func: readDir, 308 }); 309 // Check that we can read the file named `font` which typically 310 // exists in the the font registry directory. 311 let fontFile = fontRegistryDir.clone(); 312 fontFile.appendRelativePath("font"); 313 if (fontFile.exists()) { 314 tests.push({ 315 desc: `FontRegistry file (${fontFile.path})`, 316 ok: true, 317 browser: webBrowser, 318 file: fontFile, 319 minLevel: minHomeReadSandboxLevel(), 320 func: readFile, 321 }); 322 } 323 } 324 325 // Test that we cannot read from /Volumes at level 3 326 let volumes = GetDir("/Volumes"); 327 tests.push({ 328 desc: "/Volumes", 329 ok: false, 330 browser: webBrowser, 331 file: volumes, 332 minLevel: minHomeReadSandboxLevel(), 333 func: readDir, 334 }); 335 336 // Test that we cannot read from /Users at level 3 337 let users = GetDir("/Users"); 338 tests.push({ 339 desc: "/Users", 340 ok: false, 341 browser: webBrowser, 342 file: users, 343 minLevel: minHomeReadSandboxLevel(), 344 func: readDir, 345 }); 346 347 // Test that we can stat /Users at level 3 348 tests.push({ 349 desc: "/Users", 350 ok: true, 351 browser: webBrowser, 352 file: users, 353 minLevel: minHomeReadSandboxLevel(), 354 func: statPath, 355 }); 356 357 // Test that we can stat /Library at level 3, but can't get a 358 // directory listing of /Library. This test uses "/Library" 359 // because it's a path that is expected to always be present. 360 let libraryDir = GetDir("/Library"); 361 tests.push({ 362 desc: "/Library", 363 ok: true, 364 browser: webBrowser, 365 file: libraryDir, 366 minLevel: minHomeReadSandboxLevel(), 367 func: statPath, 368 }); 369 tests.push({ 370 desc: "/Library", 371 ok: false, 372 browser: webBrowser, 373 file: libraryDir, 374 minLevel: minHomeReadSandboxLevel(), 375 func: readDir, 376 }); 377 378 // Similarly, test that we can stat /private, but not /private/etc. 379 let privateDir = GetDir("/private"); 380 tests.push({ 381 desc: "/private", 382 ok: true, 383 browser: webBrowser, 384 file: privateDir, 385 minLevel: minHomeReadSandboxLevel(), 386 func: statPath, 387 }); 388 389 await runTestsList(tests); 390 } 391 392 async function testFileAccessLinuxOnly() { 393 if (!isLinux()) { 394 return; 395 } 396 397 let webBrowser = GetWebBrowser(); 398 let fileContentProcessEnabled = isFileContentProcessEnabled(); 399 let fileBrowser = GetFileBrowser(); 400 401 let tests = []; 402 403 // Test /proc/self/fd, because that can be used to unfreeze 404 // frozen shared memory. 405 let selfFdDir = GetDir("/proc/self/fd"); 406 tests.push({ 407 desc: "/proc/self/fd", 408 ok: false, 409 browser: webBrowser, 410 file: selfFdDir, 411 minLevel: isContentFileIOSandboxed(), 412 func: readDir, 413 }); 414 415 let cacheFontConfigDir = GetHomeSubdir(".cache/fontconfig/"); 416 tests.push({ 417 desc: `$HOME/.cache/fontconfig/ (${cacheFontConfigDir.path})`, 418 ok: true, 419 browser: webBrowser, 420 file: cacheFontConfigDir, 421 minLevel: minHomeReadSandboxLevel(), 422 func: readDir, 423 }); 424 425 // allows to handle both $HOME/.config/ or $XDG_CONFIG_HOME 426 let configDir = GetHomeSubdir(".config"); 427 428 const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME"); 429 if (xdgConfigHome) { 430 configDir = GetDir(xdgConfigHome); 431 configDir.normalize(); 432 } 433 434 tests.push({ 435 desc: `$XDG_CONFIG_HOME (${configDir.path})`, 436 ok: true, // access should not be granted outside of XDG support 437 browser: webBrowser, 438 file: configDir, 439 minLevel: minHomeReadSandboxLevel(), 440 func: readDir, 441 }); 442 443 tests.push({ 444 desc: `XDG_CONFIG_HOME=${configDir.path} dir should have rdonly`, 445 ok: true, // should be allowed only if XDG support is there 446 browser: webBrowser, 447 file: configDir, 448 minLevel: minHomeReadSandboxLevel(), 449 func: readDir, 450 }); 451 452 if (fileContentProcessEnabled) { 453 tests.push({ 454 desc: `${configDir.path} dir`, 455 ok: true, // should be allowed only if XDG support is there 456 browser: fileBrowser, 457 file: configDir, 458 minLevel: 0, 459 func: readDir, 460 }); 461 } 462 463 if (isXdgEnabled() && xdgConfigHome) { 464 const homeConfigDir = GetHomeSubdir(".config"); 465 tests.push({ 466 desc: `XDG_CONFIG_HOME=${homeConfigDir.path} dir should deny $HOME/.config`, 467 ok: false, 468 browser: webBrowser, 469 file: homeConfigDir, 470 minLevel: minHomeReadSandboxLevel(), 471 func: readDir, 472 }); 473 if (fileContentProcessEnabled) { 474 tests.push({ 475 desc: `${homeConfigDir.path} dir`, 476 ok: true, 477 browser: fileBrowser, 478 file: homeConfigDir, 479 minLevel: 0, 480 func: readDir, 481 }); 482 } 483 } else { 484 // WWhen XDG_CONFIG_HOME is not set, verify we do not allow $HOME/.configlol 485 // (i.e., check allow the dir and not the prefix) 486 // 487 // Checking $HOME/.config is already done above. 488 const homeConfigPrefix = GetHomeSubdir(".configlol"); 489 tests.push({ 490 desc: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`, 491 ok: false, 492 browser: webBrowser, 493 file: homeConfigPrefix, 494 minLevel: minHomeReadSandboxLevel(), 495 func: readDir, 496 }); 497 if (fileContentProcessEnabled) { 498 tests.push({ 499 desc: `No XDG_CONFIG_HOME we dont allow ${homeConfigPrefix.path} access`, 500 ok: false, 501 browser: fileBrowser, 502 file: homeConfigPrefix, 503 minLevel: 0, 504 func: readDir, 505 }); 506 } 507 } 508 509 // Create a file under $HOME/.config/ or $XDG_CONFIG_HOME and ensure we can 510 // read it 511 let fileUnderConfig = GetSubdirFile(configDir); 512 await IOUtils.writeUTF8(fileUnderConfig.path, "TEST FILE DUMMY DATA"); 513 ok( 514 await IOUtils.exists(fileUnderConfig.path), 515 `File ${fileUnderConfig.path} was properly created` 516 ); 517 518 tests.push({ 519 desc: `${configDir.path}/xxx is readable (${fileUnderConfig.path})`, 520 ok: true, 521 browser: webBrowser, 522 file: fileUnderConfig, 523 minLevel: minHomeReadSandboxLevel(), 524 func: readFile, 525 cleanup: aPath => IOUtils.remove(aPath), 526 }); 527 528 let configFile = GetSubdirFile(configDir); 529 tests.push({ 530 desc: `${configDir.path} file write`, 531 ok: false, 532 browser: webBrowser, 533 file: configFile, 534 minLevel: minHomeReadSandboxLevel(), 535 func: createFile, 536 }); 537 if (fileContentProcessEnabled) { 538 tests.push({ 539 desc: `${configDir.path} file write`, 540 ok: false, 541 browser: fileBrowser, 542 file: configFile, 543 minLevel: 0, 544 func: createFile, 545 }); 546 } 547 548 // Create a $HOME/.config/mozilla/ or $XDG_CONFIG_HOME/mozilla/ if none 549 // exists and assert content process cannot access it 550 let configMozilla = GetSubdir(configDir, "mozilla"); 551 const emptyFileName = ".test_run_browser_sandbox.tmp"; 552 let emptyFile = configMozilla.clone(); 553 emptyFile.appendRelativePath(emptyFileName); 554 555 let populateFakeConfigMozilla = async aPath => { 556 // called with configMozilla 557 await IOUtils.makeDirectory(aPath, { permissions: 0o700 }); 558 await IOUtils.writeUTF8(emptyFile.path, ""); 559 ok( 560 await IOUtils.exists(emptyFile.path), 561 `Temp file ${emptyFile.path} was created` 562 ); 563 }; 564 565 let unpopulateFakeConfigMozilla = async aPath => { 566 // called with emptyFile 567 await IOUtils.remove(aPath); 568 ok(!(await IOUtils.exists(aPath)), `Temp file ${aPath} was removed`); 569 const parentDir = PathUtils.parent(aPath); 570 try { 571 await IOUtils.remove(parentDir, { recursive: false }); 572 } catch (ex) { 573 if ( 574 !DOMException.isInstance(ex) || 575 ex.name !== "OperationError" || 576 /Could not remove the non-empty directory/.test(ex.message) 577 ) { 578 // If we get here it means the directory was not empty and since we assert 579 // earlier we removed the temp file we created it means we should not 580 // worrying about removing this directory ... 581 throw ex; 582 } 583 } 584 }; 585 586 await populateFakeConfigMozilla(configMozilla.path); 587 588 tests.push({ 589 desc: `stat ${configDir.path}/mozilla (${configMozilla.path})`, 590 ok: false, 591 browser: webBrowser, 592 file: configMozilla, 593 minLevel: minHomeReadSandboxLevel(), 594 func: statPath, 595 }); 596 597 tests.push({ 598 desc: `read ${configDir.path}/mozilla (${configMozilla.path})`, 599 ok: false, 600 browser: webBrowser, 601 file: configMozilla, 602 minLevel: minHomeReadSandboxLevel(), 603 func: readDir, 604 }); 605 606 tests.push({ 607 desc: `stat ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`, 608 ok: false, 609 browser: webBrowser, 610 file: emptyFile, 611 minLevel: minHomeReadSandboxLevel(), 612 func: statPath, 613 }); 614 615 tests.push({ 616 desc: `read ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`, 617 ok: false, 618 browser: webBrowser, 619 file: emptyFile, 620 minLevel: minHomeReadSandboxLevel(), 621 func: readFile, 622 cleanup: unpopulateFakeConfigMozilla, 623 }); 624 625 // Only needed to perform cleanup 626 if (isXdgEnabled()) { 627 tests.push({ 628 desc: `$XDG_CONFIG_HOME (${configDir.path}) cleanup`, 629 ok: true, 630 browser: webBrowser, 631 file: configDir, 632 minLevel: minHomeReadSandboxLevel(), 633 func: readDir, 634 }); 635 } 636 637 await runTestsList(tests); 638 } 639 640 async function testFileAccessLinuxSnap() { 641 let webBrowser = GetWebBrowser(); 642 643 let tests = []; 644 645 // Assert that if we run with SNAP= env, then we allow access to it in the 646 // content process 647 let snap = Services.env.get("SNAP"); 648 let snapExpectedResult = false; 649 if (snap.length > 1) { 650 snapExpectedResult = true; 651 } else { 652 snap = "/tmp/.snap_firefox_current/"; 653 } 654 655 let snapDir = GetDir(snap); 656 snapDir.normalize(); 657 658 let snapFile = GetSubdirFile(snapDir); 659 await createFile(snapFile.path); 660 ok(await IOUtils.exists(snapFile.path), `SNAP ${snapFile.path} was created`); 661 info(`SNAP (file) ${snapFile.path} was created`); 662 663 tests.push({ 664 desc: `$SNAP (${snapDir.path} => ${snapFile.path})`, 665 ok: snapExpectedResult, 666 browser: webBrowser, 667 file: snapFile, 668 minLevel: minHomeReadSandboxLevel(), 669 func: readFile, 670 }); 671 672 await runTestsList(tests); 673 } 674 675 async function testFileAccessWindowsOnly() { 676 if (!isWin()) { 677 return; 678 } 679 680 let webBrowser = GetWebBrowser(); 681 682 let tests = []; 683 684 let extDir = GetPerUserExtensionDir(); 685 // We used to unconditionally create this directory from Firefox, but that 686 // was dropped in bug 2001887. The value of this directory is questionable; 687 // the test was added in Firefox 56 (bug 1403744) to cover legacy add-ons, 688 // but legacy add-on support was discontinued in Firefox 57, and we stopped 689 // sideloading add-ons from this directory on all builds except ESR in 690 // Firefox 74 (bug 1602840). 691 await IOUtils.makeDirectory(extDir.path); 692 tests.push({ 693 desc: "per-user extensions dir", 694 ok: true, 695 browser: webBrowser, 696 file: extDir, 697 minLevel: minHomeReadSandboxLevel(), 698 func: readDir, 699 }); 700 701 await runTestsList(tests); 702 } 703 704 function cleanupBrowserTabs() { 705 let fileBrowser = GetFileBrowser(); 706 if (fileBrowser.selectedTab) { 707 gBrowser.removeTab(fileBrowser.selectedTab); 708 } 709 710 let webBrowser = GetWebBrowser(); 711 if (webBrowser.selectedTab) { 712 gBrowser.removeTab(webBrowser.selectedTab); 713 } 714 715 let tab1 = gBrowser.tabs[1]; 716 if (tab1) { 717 gBrowser.removeTab(tab1); 718 } 719 }