зеркало из https://github.com/mozilla/gecko-dev.git
611 строки
18 KiB
JavaScript
611 строки
18 KiB
JavaScript
/* 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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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) {
|
|
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 ContentTask.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 ContentTask.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 ContentTask.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 ContentTask.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,
|
|
});
|
|
}
|
|
}
|
|
|
|
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,
|
|
});
|
|
// 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
|
|
// stat something within /Library. This test uses "/Library"
|
|
// because it's a path that is expected to always be present
|
|
// and isn't something content processes have read access to
|
|
// (just read-metadata).
|
|
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,
|
|
});
|
|
let libraryWidgetsDir = GetDir("/Library/Widgets");
|
|
tests.push({
|
|
desc: "/Library/Widgets",
|
|
ok: false,
|
|
browser: webBrowser,
|
|
file: libraryWidgetsDir,
|
|
minLevel: minHomeReadSandboxLevel(),
|
|
func: statPath,
|
|
});
|
|
|
|
// 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 privateEtcDir = GetFile("/private/etc");
|
|
tests.push({
|
|
desc: "/private/etc",
|
|
ok: false,
|
|
browser: webBrowser,
|
|
file: privateEtcDir,
|
|
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);
|
|
}
|
|
}
|