gecko-dev/security/sandbox/test/browser_content_sandbox_fs.js

643 строки
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) {
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);
}
}