/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /* import-globals-from browser_content_sandbox_utils.js */ "use strict"; // Test if the content process can create in $HOME, this should fail async function createFileInHome() { let browser = gBrowser.selectedBrowser; let homeFile = fileInHomeDir(); let path = homeFile.path; let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); ok(!fileCreated.ok, "creating a file in home dir is not permitted"); if (fileCreated.ok) { // content process successfully created the file, now remove it homeFile.remove(false); } } // Test if the content process can create a temp file, this is disallowed on // macOS and Windows but allowed everywhere else. Also test that the content // process cannot create symlinks on macOS or delete files. async function createTempFile() { // On Windows we allow access to the temp dir for DEBUG builds, because of // logging that uses that dir. let isOptWin = isWin() && !SpecialPowers.isDebugBuild; let browser = gBrowser.selectedBrowser; let path = fileInTempDir().path; let fileCreated = await SpecialPowers.spawn(browser, [path], createFile); if (isMac() || isOptWin) { ok(!fileCreated.ok, "creating a file in temp is not permitted"); } else { ok(!!fileCreated.ok, "creating a file in temp is permitted"); } // now delete the file let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile); if (isMac() || isOptWin) { // On macOS we do not allow file deletion - it is not needed by the content // process itself, and macOS uses a different permission to control access // so revoking it is easy. ok(!fileDeleted.ok, "deleting a file in temp is not permitted"); } else { ok(!!fileDeleted.ok, "deleting a file in temp is permitted"); } // Test that symlink creation is not allowed on macOS. if (isMac()) { let path = fileInTempDir().path; let symlinkCreated = await SpecialPowers.spawn( browser, [path], createSymlink ); ok(!symlinkCreated.ok, "created a symlink in temp is not permitted"); } } // Test reading files and dirs from web and file content processes. async function testFileAccessAllPlatforms() { let webBrowser = GetWebBrowser(); let fileContentProcessEnabled = isFileContentProcessEnabled(); let fileBrowser = GetFileBrowser(); // Directories/files to test accessing from content processes. // For directories, we test whether a directory listing is allowed // or blocked. For files, we test if we can read from the file. // Each entry in the array represents a test file or directory // that will be read from either a web or file process. let tests = []; let profileDir = GetProfileDir(); tests.push({ desc: "profile dir", // description ok: false, // expected to succeed? browser: webBrowser, // browser to run test in file: profileDir, // nsIFile object minLevel: minProfileReadSandboxLevel(), // min level to enable test func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: "profile dir", ok: true, browser: fileBrowser, file: profileDir, minLevel: 0, func: readDir, }); } let homeDir = GetHomeDir(); tests.push({ desc: "home dir", ok: false, browser: webBrowser, file: homeDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: "home dir", ok: true, browser: fileBrowser, file: homeDir, minLevel: 0, func: readDir, }); } let extensionsDir = GetProfileEntry("extensions"); if (extensionsDir.exists() && extensionsDir.isDirectory()) { tests.push({ desc: "extensions dir", ok: true, browser: webBrowser, file: extensionsDir, minLevel: 0, func: readDir, }); } else { ok(false, `${extensionsDir.path} is a valid dir`); } let chromeDir = GetProfileEntry("chrome"); if (chromeDir.exists() && chromeDir.isDirectory()) { tests.push({ desc: "chrome dir", ok: true, browser: webBrowser, file: chromeDir, minLevel: 0, func: readDir, }); } else { ok(false, `${chromeDir.path} is valid dir`); } let cookiesFile = GetProfileEntry("cookies.sqlite"); if (cookiesFile.exists() && !cookiesFile.isDirectory()) { tests.push({ desc: "cookies file", ok: false, browser: webBrowser, file: cookiesFile, minLevel: minProfileReadSandboxLevel(), func: readFile, }); if (fileContentProcessEnabled) { tests.push({ desc: "cookies file", ok: true, browser: fileBrowser, file: cookiesFile, minLevel: 0, func: readFile, }); } } else { ok(false, `${cookiesFile.path} is a valid file`); } if (isMac() || isLinux()) { let varDir = GetDir("/var"); if (isMac()) { // Mac sandbox rules use /private/var because /var is a symlink // to /private/var on OS X. Make sure that hasn't changed. varDir.normalize(); Assert.ok( varDir.path === "/private/var", "/var resolves to /private/var" ); } tests.push({ desc: "/var", ok: false, browser: webBrowser, file: varDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: "/var", ok: true, browser: fileBrowser, file: varDir, minLevel: 0, func: readDir, }); } } await runTestsList(tests); } async function testFileAccessMacOnly() { if (!isMac()) { return; } let webBrowser = GetWebBrowser(); let fileContentProcessEnabled = isFileContentProcessEnabled(); let fileBrowser = GetFileBrowser(); let level = GetSandboxLevel(); let tests = []; // If ~/Library/Caches/TemporaryItems exists, when level <= 2 we // make sure it's readable. For level 3, we make sure it isn't. let homeTempDir = GetHomeDir(); homeTempDir.appendRelativePath("Library/Caches/TemporaryItems"); if (homeTempDir.exists()) { let shouldBeReadable, minLevel; if (level >= minHomeReadSandboxLevel()) { shouldBeReadable = false; minLevel = minHomeReadSandboxLevel(); } else { shouldBeReadable = true; minLevel = 0; } tests.push({ desc: "home library cache temp dir", ok: shouldBeReadable, browser: webBrowser, file: homeTempDir, minLevel, func: readDir, }); } // Test if we can read from $TMPDIR because we expect it // to be within /private/var. Reading from it should be // prevented in a 'web' process. let macTempDir = GetDirFromEnvVariable("TMPDIR"); macTempDir.normalize(); Assert.ok( macTempDir.path.startsWith("/private/var"), "$TMPDIR is in /private/var" ); tests.push({ desc: `$TMPDIR (${macTempDir.path})`, ok: false, browser: webBrowser, file: macTempDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: `$TMPDIR (${macTempDir.path})`, ok: true, browser: fileBrowser, file: macTempDir, minLevel: 0, func: readDir, }); } // The font registry directory is in the Darwin user cache dir which is // accessible with the getconf(1) library call using DARWIN_USER_CACHE_DIR. // For this test, assume the cache dir is located at $TMPDIR/../C and use // the $TMPDIR to derive the path to the registry. let fontRegistryDir = macTempDir.parent.clone(); fontRegistryDir.appendRelativePath("C/com.apple.FontRegistry"); if (fontRegistryDir.exists()) { tests.push({ desc: `FontRegistry (${fontRegistryDir.path})`, ok: true, browser: webBrowser, file: fontRegistryDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); // Check that we can read the file named `font` which typically // exists in the the font registry directory. let fontFile = fontRegistryDir.clone(); fontFile.appendRelativePath("font"); if (fontFile.exists()) { tests.push({ desc: `FontRegistry file (${fontFile.path})`, ok: true, browser: webBrowser, file: fontFile, minLevel: minHomeReadSandboxLevel(), func: readFile, }); } } // Test that we cannot read from /Volumes at level 3 let volumes = GetDir("/Volumes"); tests.push({ desc: "/Volumes", ok: false, browser: webBrowser, file: volumes, minLevel: minHomeReadSandboxLevel(), func: readDir, }); // /Network is not present on macOS 10.15 (xnu 19). Don't // test this directory on 10.15 and later. if (AppConstants.isPlatformAndVersionAtMost("macosx", 18)) { // Test that we cannot read from /Network at level 3 let network = GetDir("/Network"); tests.push({ desc: "/Network", ok: false, browser: webBrowser, file: network, minLevel: minHomeReadSandboxLevel(), func: readDir, }); } // Test that we cannot read from /Users at level 3 let users = GetDir("/Users"); tests.push({ desc: "/Users", ok: false, browser: webBrowser, file: users, minLevel: minHomeReadSandboxLevel(), func: readDir, }); // Test that we can stat /Users at level 3 tests.push({ desc: "/Users", ok: true, browser: webBrowser, file: users, minLevel: minHomeReadSandboxLevel(), func: statPath, }); // Test that we can stat /Library at level 3, but can't get a // directory listing of /Library. This test uses "/Library" // because it's a path that is expected to always be present. let libraryDir = GetDir("/Library"); tests.push({ desc: "/Library", ok: true, browser: webBrowser, file: libraryDir, minLevel: minHomeReadSandboxLevel(), func: statPath, }); tests.push({ desc: "/Library", ok: false, browser: webBrowser, file: libraryDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); // Similarly, test that we can stat /private, but not /private/etc. let privateDir = GetDir("/private"); tests.push({ desc: "/private", ok: true, browser: webBrowser, file: privateDir, minLevel: minHomeReadSandboxLevel(), func: statPath, }); await runTestsList(tests); } async function testFileAccessLinuxOnly() { if (!isLinux()) { return; } let webBrowser = GetWebBrowser(); let fileContentProcessEnabled = isFileContentProcessEnabled(); let fileBrowser = GetFileBrowser(); let tests = []; // Test /proc/self/fd, because that can be used to unfreeze // frozen shared memory. let selfFdDir = GetDir("/proc/self/fd"); tests.push({ desc: "/proc/self/fd", ok: false, browser: webBrowser, file: selfFdDir, minLevel: isContentFileIOSandboxed(), func: readDir, }); let cacheFontConfigDir = GetHomeSubdir(".cache/fontconfig/"); tests.push({ desc: `$HOME/.cache/fontconfig/ (${cacheFontConfigDir.path})`, ok: true, browser: webBrowser, file: cacheFontConfigDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); // allows to handle both $HOME/.config/ or $XDG_CONFIG_HOME let configDir = GetHomeSubdir(".config"); const xdgConfigHome = Services.env.get("XDG_CONFIG_HOME"); if (xdgConfigHome.length > 1) { configDir = GetDir(xdgConfigHome); configDir.normalize(); tests.push({ desc: `$XDG_CONFIG_HOME (${configDir.path})`, ok: true, browser: webBrowser, file: configDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); } // $HOME/.config/ or $XDG_CONFIG_HOME/ should have rdonly access tests.push({ desc: `${configDir.path} dir`, ok: true, browser: webBrowser, file: configDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: `${configDir.path} dir`, ok: true, browser: fileBrowser, file: configDir, minLevel: 0, func: readDir, }); } if (xdgConfigHome.length > 1) { // When XDG_CONFIG_HOME is set, dont allow $HOME/.config const homeConfigDir = GetHomeSubdir(".config"); tests.push({ desc: `${homeConfigDir.path} dir`, ok: false, browser: webBrowser, file: homeConfigDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: `${homeConfigDir.path} dir`, ok: true, browser: fileBrowser, file: homeConfigDir, minLevel: 0, func: readDir, }); } } else { // WWhen XDG_CONFIG_HOME is not set, verify we do not allow $HOME/.configlol // (i.e., check allow the dir and not the prefix) // // Checking $HOME/.config is already done above. const homeConfigPrefix = GetHomeSubdir(".configlol"); tests.push({ desc: `${homeConfigPrefix.path} dir`, ok: false, browser: webBrowser, file: homeConfigPrefix, minLevel: minHomeReadSandboxLevel(), func: readDir, }); if (fileContentProcessEnabled) { tests.push({ desc: `${homeConfigPrefix.path} dir`, ok: false, browser: fileBrowser, file: homeConfigPrefix, minLevel: 0, func: readDir, }); } } // Create a file under $HOME/.config/ or $XDG_CONFIG_HOME and ensure we can // read it let fileUnderConfig = GetSubdirFile(configDir); await IOUtils.writeUTF8(fileUnderConfig.path, "TEST FILE DUMMY DATA"); ok( await IOUtils.exists(fileUnderConfig.path), `File ${fileUnderConfig.path} was properly created` ); tests.push({ desc: `${configDir.path}/xxx is readable (${fileUnderConfig.path})`, ok: true, browser: webBrowser, file: fileUnderConfig, minLevel: minHomeReadSandboxLevel(), func: readFile, cleanup: aPath => IOUtils.remove(aPath), }); let configFile = GetSubdirFile(configDir); tests.push({ desc: `${configDir.path} file write`, ok: false, browser: webBrowser, file: configFile, minLevel: minHomeReadSandboxLevel(), func: createFile, }); if (fileContentProcessEnabled) { tests.push({ desc: `${configDir.path} file write`, ok: false, browser: fileBrowser, file: configFile, minLevel: 0, func: createFile, }); } // Create a $HOME/.config/mozilla/ or $XDG_CONFIG_HOME/mozilla/ if none // exists and assert content process cannot access it let configMozilla = GetSubdir(configDir, "mozilla"); const emptyFileName = ".test_run_browser_sandbox.tmp"; let emptyFile = configMozilla.clone(); emptyFile.appendRelativePath(emptyFileName); let populateFakeConfigMozilla = async aPath => { // called with configMozilla await IOUtils.makeDirectory(aPath, { permissions: 0o700 }); await IOUtils.writeUTF8(emptyFile.path, ""); ok( await IOUtils.exists(emptyFile.path), `Temp file ${emptyFile.path} was created` ); }; let unpopulateFakeConfigMozilla = async aPath => { // called with emptyFile await IOUtils.remove(aPath); ok(!(await IOUtils.exists(aPath)), `Temp file ${aPath} was removed`); const parentDir = PathUtils.parent(aPath); try { await IOUtils.remove(parentDir, { recursive: false }); } catch (ex) { if ( !DOMException.isInstance(ex) || ex.name !== "OperationError" || /Could not remove the non-empty directory/.test(ex.message) ) { // If we get here it means the directory was not empty and since we assert // earlier we removed the temp file we created it means we should not // worrying about removing this directory ... throw ex; } } }; await populateFakeConfigMozilla(configMozilla.path); tests.push({ desc: `stat ${configDir.path}/mozilla (${configMozilla.path})`, ok: false, browser: webBrowser, file: configMozilla, minLevel: minHomeReadSandboxLevel(), func: statPath, }); tests.push({ desc: `read ${configDir.path}/mozilla (${configMozilla.path})`, ok: false, browser: webBrowser, file: configMozilla, minLevel: minHomeReadSandboxLevel(), func: readDir, }); tests.push({ desc: `stat ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`, ok: false, browser: webBrowser, file: emptyFile, minLevel: minHomeReadSandboxLevel(), func: statPath, }); tests.push({ desc: `read ${configDir.path}/mozilla/${emptyFileName} (${emptyFile.path})`, ok: false, browser: webBrowser, file: emptyFile, minLevel: minHomeReadSandboxLevel(), func: readFile, cleanup: unpopulateFakeConfigMozilla, }); // Only needed to perform cleanup if (xdgConfigHome.length > 1) { tests.push({ desc: `$XDG_CONFIG_HOME (${configDir.path}) cleanup`, ok: true, browser: webBrowser, file: configDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); } await runTestsList(tests); } async function testFileAccessLinuxSnap() { let webBrowser = GetWebBrowser(); let tests = []; // Assert that if we run with SNAP= env, then we allow access to it in the // content process let snap = Services.env.get("SNAP"); let snapExpectedResult = false; if (snap.length > 1) { snapExpectedResult = true; } else { snap = "/tmp/.snap_firefox_current/"; } let snapDir = GetDir(snap); snapDir.normalize(); let snapFile = GetSubdirFile(snapDir); await createFile(snapFile.path); ok(await IOUtils.exists(snapFile.path), `SNAP ${snapFile.path} was created`); info(`SNAP (file) ${snapFile.path} was created`); tests.push({ desc: `$SNAP (${snapDir.path} => ${snapFile.path})`, ok: snapExpectedResult, browser: webBrowser, file: snapFile, minLevel: minHomeReadSandboxLevel(), func: readFile, }); await runTestsList(tests); } async function testFileAccessWindowsOnly() { if (!isWin()) { return; } let webBrowser = GetWebBrowser(); let tests = []; let extDir = GetPerUserExtensionDir(); tests.push({ desc: "per-user extensions dir", ok: true, browser: webBrowser, file: extDir, minLevel: minHomeReadSandboxLevel(), func: readDir, }); await runTestsList(tests); } function cleanupBrowserTabs() { let fileBrowser = GetFileBrowser(); if (fileBrowser.selectedTab) { gBrowser.removeTab(fileBrowser.selectedTab); } let webBrowser = GetWebBrowser(); if (webBrowser.selectedTab) { gBrowser.removeTab(webBrowser.selectedTab); } let tab1 = gBrowser.tabs[1]; if (tab1) { gBrowser.removeTab(tab1); } }