summaryrefslogtreecommitdiffstats
path: root/security/sandbox/test/browser_content_sandbox_fs.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /security/sandbox/test/browser_content_sandbox_fs.js
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/sandbox/test/browser_content_sandbox_fs.js')
-rw-r--r--security/sandbox/test/browser_content_sandbox_fs.js642
1 files changed, 642 insertions, 0 deletions
diff --git a/security/sandbox/test/browser_content_sandbox_fs.js b/security/sandbox/test/browser_content_sandbox_fs.js
new file mode 100644
index 0000000000..967f98c856
--- /dev/null
+++ b/security/sandbox/test/browser_content_sandbox_fs.js
@@ -0,0 +1,642 @@
+/* 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";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/" +
+ "security/sandbox/test/browser_content_sandbox_utils.js",
+ this
+);
+
+/*
+ * This test exercises file I/O from web and file content processes using
+ * OS.File methods to validate that calls that are meant to be blocked by
+ * content sandboxing are blocked.
+ */
+
+// Creates file at |path| and returns a promise that resolves with true
+// if the file was successfully created, otherwise false. Include imports
+// so this can be safely serialized and run remotely by ContentTask.spawn.
+function createFile(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ let encoder = new TextEncoder();
+ let array = encoder.encode("TEST FILE DUMMY DATA");
+ return OS.File.writeAtomic(path, array).then(
+ function(value) {
+ return true;
+ },
+ function(reason) {
+ return false;
+ }
+ );
+}
+
+// Creates a symlink at |path| and returns a promise that resolves with true
+// if the symlink was successfully created, otherwise false. Include imports
+// so this can be safely serialized and run remotely by ContentTask.spawn.
+function createSymlink(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ // source location for the symlink can be anything
+ return OS.File.unixSymLink("/Users", path).then(
+ function(value) {
+ return true;
+ },
+ function(reason) {
+ return false;
+ }
+ );
+}
+
+// Deletes file at |path| and returns a promise that resolves with true
+// if the file was successfully deleted, otherwise false. Include imports
+// so this can be safely serialized and run remotely by ContentTask.spawn.
+function deleteFile(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ return OS.File.remove(path, { ignoreAbsent: false })
+ .then(function(value) {
+ return true;
+ })
+ .catch(function(err) {
+ return false;
+ });
+}
+
+// Reads the directory at |path| and returns a promise that resolves when
+// iteration over the directory finishes or encounters an error. The promise
+// resolves with an object where .ok indicates success or failure and
+// .numEntries is the number of directory entries found.
+function readDir(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ let numEntries = 0;
+ let iterator = new OS.File.DirectoryIterator(path);
+ let promise = iterator
+ .forEach(function(dirEntry) {
+ numEntries++;
+ })
+ .then(function() {
+ iterator.close();
+ return { ok: true, numEntries };
+ })
+ .catch(function() {
+ return { ok: false, numEntries };
+ });
+ return promise;
+}
+
+// Reads the file at |path| and returns a promise that resolves when
+// reading is completed. Returned object has boolean .ok to indicate
+// success or failure.
+function readFile(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ let promise = OS.File.read(path)
+ .then(function(binaryData) {
+ return { ok: true };
+ })
+ .catch(function(error) {
+ return { ok: false };
+ });
+ return promise;
+}
+
+// Does a stat of |path| and returns a promise that resolves if the
+// stat is successful. Returned object has boolean .ok to indicate
+// success or failure.
+function statPath(path) {
+ const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+ let promise = OS.File.stat(path)
+ .then(function(stat) {
+ return { ok: true };
+ })
+ .catch(function(error) {
+ return { ok: false };
+ });
+ return promise;
+}
+
+// Returns true if the current content sandbox level, passed in
+// the |level| argument, supports filesystem sandboxing.
+function isContentFileIOSandboxed(level) {
+ let fileIOSandboxMinLevel = 0;
+
+ // Set fileIOSandboxMinLevel to the lowest level that has
+ // content filesystem sandboxing enabled. For now, this
+ // varies across Windows, Mac, Linux, other.
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ fileIOSandboxMinLevel = 1;
+ break;
+ case "Darwin":
+ fileIOSandboxMinLevel = 1;
+ break;
+ case "Linux":
+ fileIOSandboxMinLevel = 2;
+ break;
+ default:
+ Assert.ok(false, "Unknown OS");
+ }
+
+ return level >= fileIOSandboxMinLevel;
+}
+
+// Returns the lowest sandbox level where blanket reading of the profile
+// directory from the content process should be blocked by the sandbox.
+function minProfileReadSandboxLevel(level) {
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ return 3;
+ case "Darwin":
+ return 2;
+ case "Linux":
+ return 3;
+ default:
+ Assert.ok(false, "Unknown OS");
+ return 0;
+ }
+}
+
+// Returns the lowest sandbox level where blanket reading of the home
+// directory from the content process should be blocked by the sandbox.
+function minHomeReadSandboxLevel(level) {
+ switch (Services.appinfo.OS) {
+ case "WINNT":
+ return 3;
+ case "Darwin":
+ return 3;
+ case "Linux":
+ return 3;
+ default:
+ Assert.ok(false, "Unknown OS");
+ return 0;
+ }
+}
+
+//
+// Checks that sandboxing is enabled and at the appropriate level
+// setting before triggering tests that do the file I/O.
+//
+// Tests attempting to write to a file in the home directory from the
+// content process--expected to fail.
+//
+// Tests attempting to write to a file in the content temp directory
+// from the content process--expected to succeed. Uses "ContentTmpD".
+//
+// Tests reading various files and directories from file and web
+// content processes.
+//
+add_task(async function() {
+ // This test is only relevant in e10s
+ if (!gMultiProcessBrowser) {
+ ok(false, "e10s is enabled");
+ info("e10s is not enabled, exiting");
+ return;
+ }
+
+ let level = 0;
+ let prefExists = true;
+
+ // Read the security.sandbox.content.level pref.
+ // eslint-disable-next-line mozilla/use-default-preference-values
+ try {
+ level = Services.prefs.getIntPref("security.sandbox.content.level");
+ } catch (e) {
+ prefExists = false;
+ }
+
+ ok(prefExists, "pref security.sandbox.content.level exists");
+ if (!prefExists) {
+ return;
+ }
+
+ info(`security.sandbox.content.level=${level}`);
+ ok(level > 0, "content sandbox is enabled.");
+
+ let isFileIOSandboxed = isContentFileIOSandboxed(level);
+
+ // Content sandbox enabled, but level doesn't include file I/O sandboxing.
+ ok(isFileIOSandboxed, "content file I/O sandboxing is enabled.");
+ if (!isFileIOSandboxed) {
+ info("content sandbox level too low for file I/O tests, exiting\n");
+ return;
+ }
+
+ // Test creating a file in the home directory from a web content process
+ add_task(createFileInHome);
+
+ // Test creating a file content temp from a web content process
+ add_task(createTempFile);
+
+ // Test reading files/dirs from web and file content processes
+ add_task(testFileAccess);
+});
+
+// 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, "creating a file in home dir is not permitted");
+ if (fileCreated) {
+ // 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 but allowed everywhere else. Also test that the content process cannot
+// create symlinks or delete files.
+async function createTempFile() {
+ let browser = gBrowser.selectedBrowser;
+ let path = fileInTempDir().path;
+ let fileCreated = await SpecialPowers.spawn(browser, [path], createFile);
+ if (isMac()) {
+ ok(!fileCreated, "creating a file in content temp is not permitted");
+ } else {
+ ok(!!fileCreated, "creating a file in content temp is permitted");
+ }
+ // now delete the file
+ let fileDeleted = await SpecialPowers.spawn(browser, [path], deleteFile);
+ if (isMac()) {
+ // 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, "deleting a file in content temp is not permitted");
+
+ let path = fileInTempDir().path;
+ let symlinkCreated = await SpecialPowers.spawn(
+ browser,
+ [path],
+ createSymlink
+ );
+ ok(!symlinkCreated, "created a symlink in content temp is not permitted");
+ } else {
+ ok(!!fileDeleted, "deleting a file in content temp is permitted");
+ }
+}
+
+// Test reading files and dirs from web and file content processes.
+async function testFileAccess() {
+ // for tests that run in a web content process
+ let webBrowser = gBrowser.selectedBrowser;
+
+ // Ensure that the file content process is enabled.
+ let fileContentProcessEnabled = Services.prefs.getBoolPref(
+ "browser.tabs.remote.separateFileUriProcess"
+ );
+ ok(fileContentProcessEnabled, "separate file content process is enabled");
+
+ // for tests that run in a file content process
+ let fileBrowser = undefined;
+ if (fileContentProcessEnabled) {
+ // open a tab in a file content process
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", {
+ preferredRemoteType: "file",
+ });
+ // get the browser for the file content process tab
+ fileBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
+ }
+
+ // Current level
+ let level = Services.prefs.getIntPref("security.sandbox.content.level");
+
+ // 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 sysExtDevDir = GetSystemExtensionsDevDir();
+ tests.push({
+ desc: "system extensions dev dir",
+ ok: true,
+ browser: webBrowser,
+ file: sysExtDevDir,
+ minLevel: 0,
+ func: readDir,
+ });
+
+ if (isWin()) {
+ let extDir = GetPerUserExtensionDir();
+ tests.push({
+ desc: "per-user extensions dir",
+ ok: true,
+ browser: webBrowser,
+ file: extDir,
+ minLevel: minHomeReadSandboxLevel(),
+ func: readDir,
+ });
+ }
+
+ if (isMac()) {
+ // 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,
+ });
+ }
+ }
+
+ 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,
+ });
+ }
+ }
+
+ // Test /proc/self/fd, because that can be used to unfreeze
+ // frozen shared memory.
+ if (isLinux()) {
+ let selfFdDir = GetDir("/proc/self/fd");
+
+ tests.push({
+ desc: "/proc/self/fd",
+ ok: false,
+ browser: webBrowser,
+ file: selfFdDir,
+ minLevel: isContentFileIOSandboxed(),
+ func: readDir,
+ });
+ }
+
+ if (isMac()) {
+ // 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,
+ });
+ }
+
+ // 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,
+ });
+ }
+
+ 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`);
+ }
+
+ // remove tests not enabled by the current sandbox level
+ tests = tests.filter(test => test.minLevel <= level);
+
+ for (let test of tests) {
+ let okString = test.ok ? "allowed" : "blocked";
+ let processType = test.browser === webBrowser ? "web" : "file";
+
+ // ensure the file/dir exists before we ask a content process to stat
+ // it so we know a failure is not due to a nonexistent file/dir
+ if (test.func === statPath) {
+ ok(test.file.exists(), `${test.file.path} exists`);
+ }
+
+ let result = await ContentTask.spawn(
+ test.browser,
+ test.file.path,
+ test.func
+ );
+
+ ok(
+ result.ok == test.ok,
+ `reading ${test.desc} from a ${processType} process ` +
+ `is ${okString} (${test.file.path})`
+ );
+
+ // if the directory is not expected to be readable,
+ // ensure the listing has zero entries
+ if (test.func === readDir && !test.ok) {
+ ok(result.numEntries == 0, `directory list is empty (${test.file.path})`);
+ }
+ }
+
+ if (fileContentProcessEnabled) {
+ gBrowser.removeTab(gBrowser.selectedTab);
+ }
+}