From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../test/browser_content_sandbox_fs_tests.js | 698 +++++++++++++++++++++ 1 file changed, 698 insertions(+) create mode 100644 security/sandbox/test/browser_content_sandbox_fs_tests.js (limited to 'security/sandbox/test/browser_content_sandbox_fs_tests.js') diff --git a/security/sandbox/test/browser_content_sandbox_fs_tests.js b/security/sandbox/test/browser_content_sandbox_fs_tests.js new file mode 100644 index 0000000000..12678ddbe0 --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_fs_tests.js @@ -0,0 +1,698 @@ +/* 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); + } +} -- cgit v1.2.3