diff options
Diffstat (limited to 'security/sandbox/test')
-rw-r--r-- | security/sandbox/test/.eslintrc.js | 5 | ||||
-rw-r--r-- | security/sandbox/test/browser.ini | 22 | ||||
-rw-r--r-- | security/sandbox/test/browser_bug1393259.js | 206 | ||||
-rw-r--r-- | security/sandbox/test/browser_content_sandbox_fs.js | 642 | ||||
-rw-r--r-- | security/sandbox/test/browser_content_sandbox_syscalls.js | 270 | ||||
-rw-r--r-- | security/sandbox/test/browser_content_sandbox_utils.js | 108 | ||||
-rw-r--r-- | security/sandbox/test/browser_sandbox_test.js | 48 | ||||
-rw-r--r-- | security/sandbox/test/bug1393259.html | 19 | ||||
-rwxr-xr-x | security/sandbox/test/mac_register_font.py | 86 |
9 files changed, 1406 insertions, 0 deletions
diff --git a/security/sandbox/test/.eslintrc.js b/security/sandbox/test/.eslintrc.js new file mode 100644 index 0000000000..e05d8ca741 --- /dev/null +++ b/security/sandbox/test/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: "plugin:mozilla/browser-test", +}; diff --git a/security/sandbox/test/browser.ini b/security/sandbox/test/browser.ini new file mode 100644 index 0000000000..c537d5d1b5 --- /dev/null +++ b/security/sandbox/test/browser.ini @@ -0,0 +1,22 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ +[DEFAULT] +tags = contentsandbox +support-files = + browser_content_sandbox_utils.js + mac_register_font.py + ../../../layout/reftests/fonts/fira/FiraSans-Regular.otf + +[browser_content_sandbox_fs.js] +skip-if = !e10s || (debug && os == 'win') # bug 1379635 + +[browser_content_sandbox_syscalls.js] +skip-if = !e10s + +[browser_bug1393259.js] +support-files = + bug1393259.html +skip-if = !e10s || (os != 'mac') # This is a Mac-specific test + +[browser_sandbox_test.js] +skip-if = !debug diff --git a/security/sandbox/test/browser_bug1393259.js b/security/sandbox/test/browser_bug1393259.js new file mode 100644 index 0000000000..0bc543f70e --- /dev/null +++ b/security/sandbox/test/browser_bug1393259.js @@ -0,0 +1,206 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +/* + * This test validates that an OTF font installed in a directory not + * accessible to content processes is rendered correctly by checking that + * content displayed never uses the OS fallback font "LastResort". When + * a content process renders a page with the fallback font, that is an + * indication the content process failed to read or load the computed font. + * The test uses a version of the Fira Sans font and depends on the font + * not being already installed and enabled. + */ + +const kPageURL = + "http://example.com/browser/security/sandbox/test/bug1393259.html"; + +const environment = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment +); + +// Parameters for running the python script that registers/unregisters fonts. +const kPythonPath = "/usr/bin/python"; +const kFontInstallerPath = "browser/security/sandbox/test/mac_register_font.py"; +const kUninstallFlag = "-u"; +const kVerboseFlag = "-v"; + +// Where to find the font in the test environment. +const kRepoFontPath = "browser/security/sandbox/test/FiraSans-Regular.otf"; + +// Font name strings to check for. +const kLastResortFontName = "LastResort"; +const kTestFontName = "Fira Sans"; + +// Home-relative path to install a private font. Where a private font is +// a font at a location not readable by content processes. +const kPrivateFontSubPath = "/FiraSans-Regular.otf"; + +add_task(async function() { + await new Promise(resolve => waitForFocus(resolve, window)); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: kPageURL, + }, + async function(aBrowser) { + function runProcess(aCmd, aArgs, blocking = true) { + let cmdFile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + cmdFile.initWithPath(aCmd); + + let process = Cc["@mozilla.org/process/util;1"].createInstance( + Ci.nsIProcess + ); + process.init(cmdFile); + process.run(blocking, aArgs, aArgs.length); + return process.exitValue; + } + + // Register the font at path |fontPath| and wait + // for the browser to detect the change. + async function registerFont(fontPath) { + let fontRegistered = getFontNotificationPromise(); + let exitCode = runProcess(kPythonPath, [ + kFontInstallerPath, + kVerboseFlag, + fontPath, + ]); + Assert.ok(exitCode == 0, "registering font" + fontPath); + if (exitCode == 0) { + // Wait for the font registration to be detected by the browser. + await fontRegistered; + } + } + + // Unregister the font at path |fontPath|. If |waitForUnreg| is true, + // don't wait for the browser to detect the change and don't use + // the verbose arg for the unregister command. + async function unregisterFont(fontPath, waitForUnreg = true) { + let args = [kFontInstallerPath, kUninstallFlag]; + let fontUnregistered; + + if (waitForUnreg) { + args.push(kVerboseFlag); + fontUnregistered = getFontNotificationPromise(); + } + + let exitCode = runProcess(kPythonPath, args.concat(fontPath)); + if (waitForUnreg) { + Assert.ok(exitCode == 0, "unregistering font" + fontPath); + if (exitCode == 0) { + await fontUnregistered; + } + } + } + + // Pref "font.internaluseonly.changed" is updated when system + // fonts change. We use it to wait for changes to be detected + // in the browser. + let prefBranch = Services.prefs.getBranch("font.internaluseonly."); + + // Returns a promise that resolves when the pref is changed + let getFontNotificationPromise = () => + new Promise(resolve => { + let prefObserver = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe() { + prefBranch.removeObserver("changed", prefObserver); + resolve(); + }, + }; + prefBranch.addObserver("changed", prefObserver); + }); + + let homeDir = Services.dirsvc.get("Home", Ci.nsIFile); + let privateFontPath = homeDir.path + kPrivateFontSubPath; + + registerCleanupFunction(function() { + unregisterFont(privateFontPath, /* waitForUnreg = */ false); + runProcess("/bin/rm", [privateFontPath], /* blocking = */ false); + }); + + // Copy the font file to the private path. + runProcess("/bin/cp", [kRepoFontPath, privateFontPath]); + + // Cleanup previous aborted tests. + unregisterFont(privateFontPath, /* waitForUnreg = */ false); + + // Get the original width, using the fallback monospaced font + let origWidth = await SpecialPowers.spawn(aBrowser, [], async function() { + let window = content.window.wrappedJSObject; + let contentDiv = window.document.getElementById("content"); + return contentDiv.offsetWidth; + }); + + // Activate the font we want to test at a non-standard path. + await registerFont(privateFontPath); + + // Assign the new font to the content. + await SpecialPowers.spawn(aBrowser, [], async function() { + let window = content.window.wrappedJSObject; + let contentDiv = window.document.getElementById("content"); + contentDiv.style.fontFamily = "'Fira Sans', monospace"; + }); + + // Wait until the width has changed, indicating the content process + // has recognized the newly-activated font. + while (true) { + let width = await SpecialPowers.spawn(aBrowser, [], async function() { + let window = content.window.wrappedJSObject; + let contentDiv = window.document.getElementById("content"); + return contentDiv.offsetWidth; + }); + if (width != origWidth) { + break; + } + // If the content wasn't ready yet, wait a little before re-checking. + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(c => setTimeout(c, 100)); + } + + // Get a list of fonts now being used to display the web content. + let fontList = await SpecialPowers.spawn(aBrowser, [], async function() { + let window = content.window.wrappedJSObject; + let range = window.document.createRange(); + let contentDiv = window.document.getElementById("content"); + range.selectNode(contentDiv); + let fonts = InspectorUtils.getUsedFontFaces(range); + + let fontList = []; + for (let i = 0; i < fonts.length; i++) { + fontList.push({ name: fonts[i].name }); + } + return fontList; + }); + + let lastResortFontUsed = false; + let testFontUsed = false; + + for (let font of fontList) { + // Did we fall back to the "LastResort" font? + if (!lastResortFontUsed && font.name.includes(kLastResortFontName)) { + lastResortFontUsed = true; + continue; + } + // Did we render using our test font as expected? + if (!testFontUsed && font.name.includes(kTestFontName)) { + testFontUsed = true; + continue; + } + } + + Assert.ok( + !lastResortFontUsed, + `The ${kLastResortFontName} fallback font was not used` + ); + + Assert.ok(testFontUsed, `The test font "${kTestFontName}" was used`); + + await unregisterFont(privateFontPath); + } + ); +}); 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); + } +} diff --git a/security/sandbox/test/browser_content_sandbox_syscalls.js b/security/sandbox/test/browser_content_sandbox_syscalls.js new file mode 100644 index 0000000000..7b95bb061f --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_syscalls.js @@ -0,0 +1,270 @@ +/* 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 is for executing system calls in content processes to validate + * that calls that are meant to be blocked by content sandboxing are blocked. + * We use the term system calls loosely so that any OS API call such as + * fopen could be included. + */ + +// Calls the native execv library function. Include imports so this can be +// safely serialized and run remotely by ContentTask.spawn. +function callExec(args) { + const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); + let { lib, cmd } = args; + let libc = ctypes.open(lib); + let exec = libc.declare( + "execv", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr + ); + let rv = exec(cmd); + libc.close(); + return rv; +} + +// Calls the native fork syscall. +function callFork(args) { + const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); + let { lib } = args; + let libc = ctypes.open(lib); + let fork = libc.declare("fork", ctypes.default_abi, ctypes.int); + let rv = fork(); + libc.close(); + return rv; +} + +// Calls the native sysctl syscall. +function callSysctl(args) { + const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); + let { lib, name } = args; + let libc = ctypes.open(lib); + let sysctlbyname = libc.declare( + "sysctlbyname", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr, + ctypes.voidptr_t, + ctypes.size_t.ptr, + ctypes.voidptr_t, + ctypes.size_t.ptr + ); + let rv = sysctlbyname(name, null, null, null, null); + libc.close(); + return rv; +} + +// Calls the native open/close syscalls. +function callOpen(args) { + const { ctypes } = ChromeUtils.import("resource://gre/modules/ctypes.jsm"); + let { lib, path, flags } = args; + let libc = ctypes.open(lib); + let open = libc.declare( + "open", + ctypes.default_abi, + ctypes.int, + ctypes.char.ptr, + ctypes.int + ); + let close = libc.declare("close", ctypes.default_abi, ctypes.int, ctypes.int); + let fd = open(path, flags); + close(fd); + libc.close(); + return fd; +} + +// open syscall flags +function openWriteCreateFlags() { + Assert.ok(isMac() || isLinux()); + if (isMac()) { + let O_WRONLY = 0x001; + let O_CREAT = 0x200; + return O_WRONLY | O_CREAT; + } + // Linux + let O_WRONLY = 0x01; + let O_CREAT = 0x40; + return O_WRONLY | O_CREAT; +} + +// Returns the name of the native library needed for native syscalls +function getOSLib() { + switch (Services.appinfo.OS) { + case "WINNT": + return "kernel32.dll"; + case "Darwin": + return "libc.dylib"; + case "Linux": + return "libc.so.6"; + default: + Assert.ok(false, "Unknown OS"); + return 0; + } +} + +// Returns a harmless command to execute with execv +function getOSExecCmd() { + Assert.ok(!isWin()); + return "/bin/cat"; +} + +// Returns true if the current content sandbox level, passed in +// the |level| argument, supports syscall sandboxing. +function areContentSyscallsSandboxed(level) { + let syscallsSandboxMinLevel = 0; + + // Set syscallsSandboxMinLevel to the lowest level that has + // syscall sandboxing enabled. For now, this varies across + // Windows, Mac, Linux, other. + switch (Services.appinfo.OS) { + case "WINNT": + syscallsSandboxMinLevel = 1; + break; + case "Darwin": + syscallsSandboxMinLevel = 1; + break; + case "Linux": + syscallsSandboxMinLevel = 1; + break; + default: + Assert.ok(false, "Unknown OS"); + } + + return level >= syscallsSandboxMinLevel; +} + +// +// Drive tests for a single content process. +// +// Tests executing OS API calls in the content process. Limited to Mac +// and Linux calls for now. +// +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. + // If the pref isn't set and we're running on Linux on !isNightly(), + // exit without failing. The Linux content sandbox is only enabled + // on Nightly at this time. + // 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 areSyscallsSandboxed = areContentSyscallsSandboxed(level); + + // Content sandbox enabled, but level doesn't include syscall sandboxing. + ok(areSyscallsSandboxed, "content syscall sandboxing is enabled."); + if (!areSyscallsSandboxed) { + info("content sandbox level too low for syscall tests, exiting\n"); + return; + } + + let browser = gBrowser.selectedBrowser; + let lib = getOSLib(); + + // use execv syscall + // (causes content process to be killed on Linux) + if (isMac()) { + // exec something harmless, this should fail + let cmd = getOSExecCmd(); + let rv = await SpecialPowers.spawn(browser, [{ lib, cmd }], callExec); + ok(rv == -1, `exec(${cmd}) is not permitted`); + } + + // use open syscall + if (isLinux() || isMac()) { + // open a file for writing in $HOME, this should fail + let path = fileInHomeDir().path; + let flags = openWriteCreateFlags(); + let fd = await SpecialPowers.spawn( + browser, + [{ lib, path, flags }], + callOpen + ); + ok(fd < 0, "opening a file for writing in home is not permitted"); + } + + // use open syscall + if (isLinux() || isMac()) { + // open a file for writing in the content temp dir, this should fail on + // macOS and work on Linux. The open handler in the content process closes + // the file for us + let path = fileInTempDir().path; + let flags = openWriteCreateFlags(); + let fd = await SpecialPowers.spawn( + browser, + [{ lib, path, flags }], + callOpen + ); + if (isMac()) { + ok( + fd === -1, + "opening a file for writing in content temp is not permitted" + ); + } else { + ok(fd >= 0, "opening a file for writing in content temp is permitted"); + } + } + + // use fork syscall + if (isLinux() || isMac()) { + let rv = await SpecialPowers.spawn(browser, [{ lib }], callFork); + ok(rv == -1, "calling fork is not permitted"); + } + + // On macOS before 10.10 the |sysctl-name| predicate didn't exist for + // filtering |sysctl| access. Check the Darwin version before running the + // tests (Darwin 14.0.0 is macOS 10.10). This branch can be removed when we + // remove support for macOS 10.9. + if (isMac() && Services.sysinfo.getProperty("version") >= "14.0.0") { + let rv = await SpecialPowers.spawn( + browser, + [{ lib, name: "kern.boottime" }], + callSysctl + ); + ok(rv == -1, "calling sysctl('kern.boottime') is not permitted"); + + rv = await SpecialPowers.spawn( + browser, + [{ lib, name: "net.inet.ip.ttl" }], + callSysctl + ); + ok(rv == -1, "calling sysctl('net.inet.ip.ttl') is not permitted"); + + rv = await SpecialPowers.spawn( + browser, + [{ lib, name: "hw.ncpu" }], + callSysctl + ); + ok(rv == 0, "calling sysctl('hw.ncpu') is permitted"); + } +}); 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..562765f239 --- /dev/null +++ b/security/sandbox/test/browser_content_sandbox_utils.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + Ci.nsIUUIDGenerator +); +const environment = Cc["@mozilla.org/process/environment;1"].getService( + Ci.nsIEnvironment +); + +/* + * Utility functions for the browser content sandbox tests. + */ + +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/<UUID>). +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/<UUID> + 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 (.../<UUID>). +function fileInTempDir() { + let contentTempKey = "ContentTmpD"; + + // get the content temp dir, make sure it exists + let ctmp = Services.dirsvc.get(contentTempKey, Ci.nsIFile); + Assert.ok(ctmp.exists(), "Content temp dir exists"); + Assert.ok(ctmp.isDirectory(), "Content 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 GetSystemExtensionsDevDir() { + return Services.dirsvc.get("XRESysExtDev", Ci.nsIFile); +} + +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(environment.get(varName)); +} + +function GetFile(path) { + let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(path); + return file; +} diff --git a/security/sandbox/test/browser_sandbox_test.js b/security/sandbox/test/browser_sandbox_test.js new file mode 100644 index 0000000000..2c4af40ca8 --- /dev/null +++ b/security/sandbox/test/browser_sandbox_test.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function test() { + waitForExplicitFinish(); + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + // Types of processes to test, taken from GeckoProcessTypes.h + var processTypes = ["tab", "gpu"]; + + // A callback called after each test-result. + Services.obs.addObserver(function result(subject, topic, data) { + let { testid, shouldPermit, wasPermitted, message } = JSON.parse(data); + ok( + shouldPermit == wasPermitted, + "Test " + + testid + + " was " + + (wasPermitted ? "" : "not ") + + "permitted. | " + + message + ); + }, "sandbox-test-result"); + + // A callback that is notified when a child process is done running tests. + var remainingTests = processTypes.length; + Services.obs.addObserver(_ => { + remainingTests = remainingTests - 1; + if (remainingTests == 0) { + // Notify SandboxTest component that it should terminate the connection + // with the child processes. + comp.finishTests(); + // Notify mochitest that all process tests are complete. + finish(); + } + }, "sandbox-test-done"); + + var comp = Cc["@mozilla.org/sandbox/sandbox-test;1"].getService( + Ci.mozISandboxTest + ); + + comp.startTests(processTypes); +} diff --git a/security/sandbox/test/bug1393259.html b/security/sandbox/test/bug1393259.html new file mode 100644 index 0000000000..b1e3cca99a --- /dev/null +++ b/security/sandbox/test/bug1393259.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"/> +</head> +<style> +#content { display: inline-block; } +.monospace_fallback { font: 3em monospace; } +</style> +<body> + +<div id="content" class="monospace_fallback"> +abcdefghijklmnopqrstuvwxyz<br> +<b>abcdefghijklmnopqrstuvwxyz</b><br> +<i>abcdefghijklmnopqrstuvwxyz</i> +</div> + +</body> +</html> diff --git a/security/sandbox/test/mac_register_font.py b/security/sandbox/test/mac_register_font.py new file mode 100755 index 0000000000..e5996fcb90 --- /dev/null +++ b/security/sandbox/test/mac_register_font.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +""" +mac_register_font.py + +Mac-specific utility command to register a font file with the OS. +""" + +from __future__ import print_function + +import CoreText +import Cocoa +import argparse +import sys + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="print verbose registration failures", + default=False, + ) + parser.add_argument( + "file", nargs="*", help="font file to register or unregister", default=[] + ) + parser.add_argument( + "-u", + "--unregister", + action="store_true", + help="unregister the provided fonts", + default=False, + ) + parser.add_argument( + "-p", + "--persist-user", + action="store_true", + help="permanently register the font", + default=False, + ) + + args = parser.parse_args() + + if args.persist_user: + scope = CoreText.kCTFontManagerScopeUser + scopeDesc = "user" + else: + scope = CoreText.kCTFontManagerScopeSession + scopeDesc = "session" + + failureCount = 0 + for fontPath in args.file: + fontURL = Cocoa.NSURL.fileURLWithPath_(fontPath) + (result, error) = register_or_unregister_font(fontURL, args.unregister, scope) + if result: + print( + "%sregistered font %s with %s scope" + % (("un" if args.unregister else ""), fontPath, scopeDesc) + ) + else: + print( + "Failed to %sregister font %s with %s scope" + % (("un" if args.unregister else ""), fontPath, scopeDesc) + ) + if args.verbose: + print(error) + failureCount += 1 + + sys.exit(failureCount) + + +def register_or_unregister_font(fontURL, unregister, scope): + return ( + CoreText.CTFontManagerUnregisterFontsForURL(fontURL, scope, None) + if unregister + else CoreText.CTFontManagerRegisterFontsForURL(fontURL, scope, None) + ) + + +if __name__ == "__main__": + main() |