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 --- .../sandbox/test/browser_content_sandbox_utils.js | 464 +++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 security/sandbox/test/browser_content_sandbox_utils.js (limited to 'security/sandbox/test/browser_content_sandbox_utils.js') diff --git a/security/sandbox/test/browser_content_sandbox_utils.js b/security/sandbox/test/browser_content_sandbox_utils.js new file mode 100644 index 0000000000..ce6ed39ff6 --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_utils.js @@ -0,0 +1,464 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const uuidGenerator = Services.uuid; + +/* + * Utility functions for the browser content sandbox tests. + */ + +function sanityChecks() { + // 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"); + } +} + +// Creates file at |path| and returns a promise that resolves with an object +// with .ok boolean to indicate 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 { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + try { + const fstream = Cc[ + "@mozilla.org/network/file-output-stream;1" + ].createInstance(Ci.nsIFileOutputStream); + + fstream.init( + new FileUtils.File(path), + -1, // readonly mode + -1, // default permissions + 0 + ); // behaviour flags + + const ostream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance( + Ci.nsIBinaryOutputStream + ); + ostream.setOutputStream(fstream); + + const data = "TEST FILE DUMMY DATA"; + ostream.writeBytes(data, data.length); + + ostream.close(); + fstream.close(); + } catch (e) { + return { ok: false }; + } + + return { ok: true }; +} + +// Creates a symlink at |path| and returns a promise that resolves with an +// object with .ok boolean to indicate 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 { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + try { + const libc = ctypes.open( + Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so" + ); + + const symlink = libc.declare( + "symlink", + ctypes.default_abi, + ctypes.int, // return value + ctypes.char.ptr, // target + ctypes.char.ptr //linkpath + ); + + if (symlink("/etc", path)) { + return { ok: false }; + } + } catch (e) { + return { ok: false }; + } + + return { ok: true }; +} + +// Deletes file at |path| and returns a promise that resolves with an object +// with .ok boolean to indicate 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 { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + try { + const file = new FileUtils.File(path); + file.remove(false); + } catch (e) { + return { ok: false }; + } + + return { ok: true }; +} + +// 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 { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + let numEntries = 0; + + try { + const file = new FileUtils.File(path); + const enumerator = file.directoryEntries; + + while (enumerator.hasMoreElements()) { + void enumerator.nextFile; + numEntries++; + } + } catch (e) { + return { ok: false, numEntries }; + } + + return { ok: true, numEntries }; +} + +// 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 { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + try { + const file = new FileUtils.File(path); + + const fstream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, -1, 0); + + const istream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + istream.setInputStream(fstream); + + const available = istream.available(); + void istream.readBytes(available); + } catch (e) { + return { ok: false }; + } + + return { ok: true }; +} + +// 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 { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + try { + const file = new FileUtils.File(path); + void file.lastModifiedTime; + } catch (e) { + return { ok: false }; + } + + return { ok: true }; +} + +// 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; + } +} + +function isMac() { + return Services.appinfo.OS == "Darwin"; +} +function isWin() { + return Services.appinfo.OS == "WINNT"; +} +function isLinux() { + return Services.appinfo.OS == "Linux"; +} + +function isNightly() { + let version = SpecialPowers.Services.appinfo.version; + return version.endsWith("a1"); +} + +function uuid() { + return uuidGenerator.generateUUID().toString(); +} + +// Returns a file object for a new file in the home dir ($HOME/). +function fileInHomeDir() { + // get home directory, make sure it exists + let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); + Assert.ok(homeDir.exists(), "Home dir exists"); + Assert.ok(homeDir.isDirectory(), "Home dir is a directory"); + + // build a file object for a new file named $HOME/ + let homeFile = homeDir.clone(); + homeFile.appendRelativePath(uuid()); + Assert.ok(!homeFile.exists(), homeFile.path + " does not exist"); + return homeFile; +} + +// Returns a file object for a new file in the content temp dir (.../). +function fileInTempDir() { + let contentTempKey = "TmpD"; + + // get the content temp dir, make sure it exists + let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile); + Assert.ok(ctmp.exists(), "Temp dir exists"); + Assert.ok(ctmp.isDirectory(), "Temp dir is a directory"); + + // build a file object for a new file in content temp + let tempFile = ctmp.clone(); + tempFile.appendRelativePath(uuid()); + Assert.ok(!tempFile.exists(), tempFile.path + " does not exist"); + return tempFile; +} + +function GetProfileDir() { + // get profile directory + let profileDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + return profileDir; +} + +function GetHomeDir() { + // get home directory + let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); + return homeDir; +} + +function GetHomeSubdir(subdir) { + return GetSubdir(GetHomeDir(), subdir); +} + +function GetHomeSubdirFile(subdir) { + return GetSubdirFile(GetHomeSubdir(subdir)); +} + +function GetSubdir(dir, subdir) { + let newSubdir = dir.clone(); + newSubdir.appendRelativePath(subdir); + return newSubdir; +} + +function GetSubdirFile(dir) { + let newFile = dir.clone(); + newFile.appendRelativePath(uuid()); + return newFile; +} + +function GetPerUserExtensionDir() { + return Services.dirsvc.get("XREUSysExt", Ci.nsIFile); +} + +// Returns a file object for the file or directory named |name| in the +// profile directory. +function GetProfileEntry(name) { + let entry = GetProfileDir(); + entry.append(name); + return entry; +} + +function GetDir(path) { + let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + dir.initWithPath(path); + Assert.ok(dir.isDirectory(), `${path} is a directory`); + return dir; +} + +function GetDirFromEnvVariable(varName) { + return GetDir(Services.env.get(varName)); +} + +function GetFile(path) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + return file; +} + +function GetBrowserType(type) { + let browserType = undefined; + + if (!GetBrowserType[type]) { + if (type === "web") { + GetBrowserType[type] = gBrowser.selectedBrowser; + } else { + // open a tab in a `type` content process + gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser, "about:blank", { + preferredRemoteType: type, + }); + // get the browser for the `type` process tab + GetBrowserType[type] = gBrowser.getBrowserForTab(gBrowser.selectedTab); + } + } + + browserType = GetBrowserType[type]; + ok( + browserType.remoteType === type, + `GetBrowserType(${type}) returns a ${type} process` + ); + return browserType; +} + +function GetWebBrowser() { + return GetBrowserType("web"); +} + +function isFileContentProcessEnabled() { + // 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"); + return fileContentProcessEnabled; +} + +function GetFileBrowser() { + if (!isFileContentProcessEnabled()) { + return undefined; + } + return GetBrowserType("file"); +} + +function GetSandboxLevel() { + // Current level + return Services.prefs.getIntPref("security.sandbox.content.level"); +} + +async function runTestsList(tests) { + let level = GetSandboxLevel(); + + // 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.remoteType; + + // 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 (test.cleanup != undefined) { + await test.cleanup(test.file.path); + } + } +} -- cgit v1.2.3