// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/351564777): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif
#include <windows.h>
#include <memory>
#include <string>
#include <vector>
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/format_macros.h"
#include "base/hash/sha1.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/path_service.h"
#include "base/process/process_info.h"
#include "base/rand_util.h"
#include "base/scoped_native_library.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/access_token.h"
#include "base/win/scoped_handle.h"
#include "base/win/scoped_process_information.h"
#include "base/win/security_descriptor.h"
#include "base/win/win_util.h"
#include "base/win/windows_version.h"
#include "build/build_config.h"
#include "sandbox/features.h"
#include "sandbox/win/src/app_container_base.h"
#include "sandbox/win/tests/common/controller.h"
#include "sandbox/win/tests/common/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace sandbox {
namespace {
const wchar_t kAppContainerSid[] =
L"S-1-15-2-3251537155-1984446955-2931258699-841473695-1938553385-"
L"924012148-2839372144";
constexpr ACProfileRegistration GetProfileRegistration() {
#if defined(ARCH_CPU_ARM64)
return ACProfileRegistration::kNoFirewall;
#else
return ACProfileRegistration::kDefault;
#endif // defined(ARCH_CPU_ARM64)
}
void DeleteProfile(const wchar_t* package_name) {
switch (GetProfileRegistration()) {
case ACProfileRegistration::kDefault:
AppContainerBase::Delete(package_name);
break;
case ACProfileRegistration::kNoFirewall:
AppContainerBase::DeleteNoFirewall(package_name);
break;
}
}
std::wstring GenerateRandomPackageName() {
return base::ASCIIToWide(base::StringPrintf(
"%016" PRIX64 "%016" PRIX64, base::RandUint64(), base::RandUint64()));
}
const char* TokenTypeToName(bool impersonation) {
return impersonation ? "Impersonation Token" : "Primary Token";
}
void CheckToken(const std::optional<base::win::AccessToken>& token,
bool impersonation,
PSECURITY_CAPABILITIES security_capabilities,
bool restricted) {
ASSERT_TRUE(token);
EXPECT_EQ(restricted, token->IsRestricted())
<< TokenTypeToName(impersonation);
EXPECT_TRUE(token->IsAppContainer()) << TokenTypeToName(impersonation);
EXPECT_EQ(token->IsImpersonation(), impersonation)
<< TokenTypeToName(impersonation);
if (impersonation) {
EXPECT_FALSE(token->IsIdentification()) << TokenTypeToName(impersonation);
}
std::optional<base::win::Sid> package_sid = token->AppContainerSid();
ASSERT_TRUE(package_sid) << TokenTypeToName(impersonation);
EXPECT_TRUE(package_sid->Equal(security_capabilities->AppContainerSid))
<< TokenTypeToName(impersonation);
std::vector<base::win::AccessToken::Group> capabilities =
token->Capabilities();
ASSERT_EQ(capabilities.size(), security_capabilities->CapabilityCount)
<< TokenTypeToName(impersonation);
for (size_t index = 0; index < capabilities.size(); ++index) {
EXPECT_EQ(capabilities[index].GetAttributes(),
security_capabilities->Capabilities[index].Attributes)
<< TokenTypeToName(impersonation);
EXPECT_TRUE(capabilities[index].GetSid().Equal(
security_capabilities->Capabilities[index].Sid))
<< TokenTypeToName(impersonation);
}
}
void CheckProcessToken(HANDLE process,
PSECURITY_CAPABILITIES security_capabilities,
bool restricted) {
CheckToken(base::win::AccessToken::FromProcess(process), false,
security_capabilities, restricted);
}
void CheckThreadToken(HANDLE thread,
PSECURITY_CAPABILITIES security_capabilities,
bool restricted) {
CheckToken(base::win::AccessToken::FromThread(thread), true,
security_capabilities, restricted);
}
// Check for LPAC using an access check. We could query for a security attribute
// but that's undocumented and has the potential to change.
void CheckLpacToken(HANDLE process) {
auto token =
base::win::AccessToken::FromProcess(process, /*impersonation=*/true);
ASSERT_TRUE(token);
constexpr ACCESS_MASK kACAccess = 0x1;
constexpr ACCESS_MASK kLPACAccess = 0x2;
base::win::SecurityDescriptor sd;
sd.set_owner(base::win::Sid(base::win::WellKnownSid::kLocalSystem));
sd.set_group(base::win::Sid(base::win::WellKnownSid::kLocalSystem));
ASSERT_TRUE(sd.SetDaclEntry(base::win::WellKnownSid::kWorld,
base::win::SecurityAccessMode::kGrant,
kACAccess | kLPACAccess, 0));
ASSERT_TRUE(sd.SetDaclEntry(base::win::WellKnownSid::kAllApplicationPackages,
base::win::SecurityAccessMode::kGrant, kACAccess,
0));
ASSERT_TRUE(sd.SetDaclEntry(
base::win::WellKnownSid::kAllRestrictedApplicationPackages,
base::win::SecurityAccessMode::kGrant, kLPACAccess, 0));
GENERIC_MAPPING generic_mapping = {};
auto result = sd.AccessCheck(*token, MAXIMUM_ALLOWED, generic_mapping);
ASSERT_TRUE(result);
ASSERT_TRUE(result->access_status);
ASSERT_EQ(kLPACAccess, result->granted_access);
}
// Generate a unique sandbox AC profile for the appcontainer based on the SHA1
// hash of the appcontainer_id. This does not need to be secure so using SHA1
// isn't a security concern.
std::wstring GetAppContainerProfileName() {
std::string sandbox_base_name = std::string("cr.sb.net");
// Create a unique app container ID for the test case. This ensures that if
// multiple tests are running concurrently they don't mess with each other's
// app containers.
std::string appcontainer_id(
testing::UnitTest::GetInstance()->current_test_info()->test_suite_name());
appcontainer_id +=
testing::UnitTest::GetInstance()->current_test_info()->name();
auto sha1 = base::SHA1HashString(appcontainer_id);
std::string profile_name =
base::StrCat({sandbox_base_name, base::HexEncode(sha1)});
// CreateAppContainerProfile requires that the profile name is at most 64
// characters but 50 on WCOS systems. The size of sha1 is a constant 40, so
// validate that the base names are sufficiently short that the total length
// is valid on all systems.
DCHECK_LE(profile_name.length(), 50U);
return base::UTF8ToWide(profile_name);
}
// Adds an app container policy similar to network service.
ResultCode AddNetworkAppContainerPolicy(TargetPolicy* policy) {
std::wstring profile_name = GetAppContainerProfileName();
ResultCode ret = policy->GetConfig()->AddAppContainerProfile(
profile_name.c_str(), GetProfileRegistration());
if (SBOX_ALL_OK != ret)
return ret;
ret = policy->GetConfig()->SetTokenLevel(USER_UNPROTECTED, USER_UNPROTECTED);
if (SBOX_ALL_OK != ret)
return ret;
AppContainer* app_container = policy->GetConfig()->GetAppContainer();
constexpr const wchar_t* kBaseCapsSt[] = {
L"lpacChromeInstallFiles", L"registryRead", L"lpacIdentityServices",
L"lpacCryptoServices"};
constexpr const base::win::WellKnownCapability kBaseCapsWK[] = {
base::win::WellKnownCapability::kPrivateNetworkClientServer,
base::win::WellKnownCapability::kInternetClient,
base::win::WellKnownCapability::kEnterpriseAuthentication};
for (const auto* cap : kBaseCapsSt) {
app_container->AddCapability(cap);
}
for (const auto cap : kBaseCapsWK) {
app_container->AddCapability(cap);
}
app_container->SetEnableLowPrivilegeAppContainer(true);
return SBOX_ALL_OK;
}
class AppContainerTest : public ::testing::Test {
public:
void SetUp() override {
if (!features::IsAppContainerSandboxSupported())
return;
package_name_ = GenerateRandomPackageName();
broker_services_ = GetBroker();
policy_ = broker_services_->CreatePolicy();
ASSERT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetProcessMitigations(
MITIGATION_HEAP_TERMINATE));
ASSERT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->AddAppContainerProfile(
package_name_.c_str(), GetProfileRegistration()));
created_profile_ = true;
}
void TearDown() override {
if (scoped_process_info_.IsValid()) {
::TerminateProcess(scoped_process_info_.process_handle(), 0);
}
if (created_profile_) {
DeleteProfile(package_name_.c_str());
}
}
protected:
void CreateProcess() {
// Get the path to the sandboxed app.
wchar_t prog_name[MAX_PATH] = {};
ASSERT_NE(DWORD{0}, ::GetModuleFileNameW(nullptr, prog_name, MAX_PATH));
PROCESS_INFORMATION process_info = {};
DWORD last_error = 0;
ResultCode result = broker_services_->SpawnTarget(
prog_name, prog_name, std::move(policy_), &last_error, &process_info);
ASSERT_EQ(SBOX_ALL_OK, result) << "Last Error: " << last_error;
scoped_process_info_.Set(process_info);
}
AppContainerBase* container() {
// For testing purposes we known the base class so cast directly.
return static_cast<AppContainerBase*>(
policy_->GetConfig()->GetAppContainer());
}
std::wstring package_name_;
bool created_profile_ = false;
raw_ptr<BrokerServices> broker_services_;
std::unique_ptr<TargetPolicy> policy_;
base::win::ScopedProcessInformation scoped_process_info_;
};
} // namespace
SBOX_TESTS_COMMAND int AppContainerEvent_Open(int argc, wchar_t** argv) {
if (argc != 1)
return SBOX_TEST_FAILED_TO_EXECUTE_COMMAND;
base::win::ScopedHandle event_open(
::OpenEvent(EVENT_ALL_ACCESS, false, argv[0]));
DWORD error_open = ::GetLastError();
if (event_open.is_valid()) {
return SBOX_TEST_SUCCEEDED;
}
if (ERROR_ACCESS_DENIED == error_open)
return SBOX_TEST_DENIED;
return SBOX_TEST_FAILED;
}
TEST(LowBoxTest, DenyOpenEventForLowBox) {
if (!features::IsAppContainerSandboxSupported())
return;
base::win::ScopedHandle event(
::CreateEvent(nullptr, false, false, kAppContainerSid));
ASSERT_TRUE(event.is_valid());
TestRunner runner(JobLevel::kUnprotected, USER_UNPROTECTED, USER_UNPROTECTED);
EXPECT_EQ(SBOX_ALL_OK,
runner.GetPolicy()->GetConfig()->SetLowBox(kAppContainerSid));
std::wstring test_str = L"AppContainerEvent_Open ";
test_str += kAppContainerSid;
EXPECT_EQ(SBOX_TEST_DENIED, runner.RunTest(test_str.c_str()));
}
TEST_F(AppContainerTest, CheckIncompatibleOptions) {
if (!created_profile_) {
return;
}
EXPECT_EQ(SBOX_ERROR_BAD_PARAMS,
policy_->GetConfig()->SetIntegrityLevel(INTEGRITY_LEVEL_UNTRUSTED));
EXPECT_EQ(SBOX_ERROR_BAD_PARAMS,
policy_->GetConfig()->SetLowBox(kAppContainerSid));
MitigationFlags expected_mitigations = 0;
MitigationFlags expected_delayed = MITIGATION_HEAP_TERMINATE;
sandbox::ResultCode expected_result = SBOX_ERROR_BAD_PARAMS;
if (base::win::GetVersion() >= base::win::Version::WIN10_RS5) {
expected_mitigations = MITIGATION_HEAP_TERMINATE;
expected_delayed = 0;
expected_result = SBOX_ALL_OK;
}
EXPECT_EQ(expected_mitigations,
policy_->GetConfig()->GetProcessMitigations());
EXPECT_EQ(expected_delayed,
policy_->GetConfig()->GetDelayedProcessMitigations());
EXPECT_EQ(expected_result, policy_->GetConfig()->SetProcessMitigations(
MITIGATION_HEAP_TERMINATE));
}
TEST_F(AppContainerTest, NoCapabilities) {
if (!created_profile_) {
return;
}
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(USER_UNPROTECTED,
USER_UNPROTECTED));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), FALSE);
CheckThreadToken(scoped_process_info_.thread_handle(),
security_capabilities.get(), FALSE);
}
TEST_F(AppContainerTest, NoCapabilitiesRestricted) {
if (!created_profile_) {
return;
}
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(
USER_RESTRICTED_SAME_ACCESS, USER_LOCKDOWN));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), TRUE);
CheckThreadToken(scoped_process_info_.thread_handle(),
security_capabilities.get(), TRUE);
}
TEST_F(AppContainerTest, WithCapabilities) {
if (!created_profile_) {
return;
}
container()->AddCapability(base::win::WellKnownCapability::kInternetClient);
container()->AddCapability(
base::win::WellKnownCapability::kInternetClientServer);
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(USER_UNPROTECTED,
USER_UNPROTECTED));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), FALSE);
CheckThreadToken(scoped_process_info_.thread_handle(),
security_capabilities.get(), FALSE);
}
TEST_F(AppContainerTest, WithCapabilitiesRestricted) {
if (!created_profile_) {
return;
}
container()->AddCapability(base::win::WellKnownCapability::kInternetClient);
container()->AddCapability(
base::win::WellKnownCapability::kInternetClientServer);
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(
USER_RESTRICTED_SAME_ACCESS, USER_LOCKDOWN));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), TRUE);
CheckThreadToken(scoped_process_info_.thread_handle(),
security_capabilities.get(), TRUE);
}
TEST_F(AppContainerTest, WithImpersonationCapabilities) {
if (!created_profile_) {
return;
}
container()->AddCapability(base::win::WellKnownCapability::kInternetClient);
container()->AddCapability(
base::win::WellKnownCapability::kInternetClientServer);
container()->AddImpersonationCapability(
base::win::WellKnownCapability::kPrivateNetworkClientServer);
container()->AddImpersonationCapability(
base::win::WellKnownCapability::kPicturesLibrary);
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(USER_UNPROTECTED,
USER_UNPROTECTED));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
SecurityCapabilities impersonation_security_capabilities(
container()->GetPackageSid(),
container()->GetImpersonationCapabilities());
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), FALSE);
CheckThreadToken(scoped_process_info_.thread_handle(),
&impersonation_security_capabilities, FALSE);
}
TEST_F(AppContainerTest, NoCapabilitiesLPAC) {
if (!features::IsAppContainerSandboxSupported())
return;
container()->SetEnableLowPrivilegeAppContainer(true);
EXPECT_EQ(SBOX_ALL_OK, policy_->GetConfig()->SetTokenLevel(USER_UNPROTECTED,
USER_UNPROTECTED));
EXPECT_EQ(SBOX_ALL_OK,
policy_->GetConfig()->SetJobLevel(JobLevel::kUnprotected, 0));
auto security_capabilities = container()->GetSecurityCapabilities();
CreateProcess();
CheckProcessToken(scoped_process_info_.process_handle(),
security_capabilities.get(), FALSE);
CheckThreadToken(scoped_process_info_.thread_handle(),
security_capabilities.get(), FALSE);
CheckLpacToken(scoped_process_info_.process_handle());
}
SBOX_TESTS_COMMAND int LoadDLL(int argc, wchar_t** argv) {
// Library here doesn't matter as long as it's in the output directory: re-use
// one from another sbox test.
base::ScopedNativeLibrary test_dll(
base::FilePath(FILE_PATH_LITERAL("sbox_integration_test_win_proc.exe")));
if (test_dll.is_valid())
return SBOX_TEST_SUCCEEDED;
return SBOX_TEST_FAILED;
}
SBOX_TESTS_COMMAND int CheckIsAppContainer(int argc, wchar_t** argv) {
if (base::IsCurrentProcessInAppContainer())
return SBOX_TEST_SUCCEEDED;
return SBOX_TEST_FAILED;
}
TEST(AppContainerLaunchTest, CheckLPACACE) {
if (!features::IsAppContainerSandboxSupported())
return;
TestRunner runner;
AddNetworkAppContainerPolicy(runner.GetPolicy());
EXPECT_EQ(SBOX_TEST_SUCCEEDED, runner.RunTest(L"LoadDLL"));
DeleteProfile(GetAppContainerProfileName().c_str());
}
TEST(AppContainerLaunchTest, IsAppContainer) {
if (!features::IsAppContainerSandboxSupported())
return;
TestRunner runner;
AddNetworkAppContainerPolicy(runner.GetPolicy());
EXPECT_EQ(SBOX_TEST_SUCCEEDED, runner.RunTest(L"CheckIsAppContainer"));
DeleteProfile(GetAppContainerProfileName().c_str());
}
TEST(AppContainerLaunchTest, IsNotAppContainer) {
TestRunner runner;
EXPECT_EQ(SBOX_TEST_FAILED, runner.RunTest(L"CheckIsAppContainer"));
}
TEST(AppContainerLaunchTest, IsAppContainerNoFirewall) {
if (!features::IsAppContainerSandboxSupported()) {
return;
}
TestRunner runner;
std::wstring package_name = GenerateRandomPackageName();
ASSERT_EQ(SBOX_ALL_OK,
runner.GetPolicy()->GetConfig()->AddAppContainerProfile(
package_name.c_str(), ACProfileRegistration::kNoFirewall));
EXPECT_EQ(SBOX_TEST_SUCCEEDED, runner.RunTest(L"CheckIsAppContainer"));
EXPECT_TRUE(AppContainerBase::DeleteNoFirewall(package_name.c_str()));
}
SBOX_TESTS_COMMAND int CreateTempFileInAppContainer(int argc, wchar_t** argv) {
if (!base::IsCurrentProcessInAppContainer()) {
return SBOX_TEST_FIRST_ERROR;
}
base::FilePath temp_file;
if (!base::CreateTemporaryFile(&temp_file)) {
return SBOX_TEST_SECOND_ERROR;
}
return SBOX_TEST_SUCCEEDED;
}
TEST(AppContainerLaunchTest, CreateTempFileNoFirewall) {
if (!features::IsAppContainerSandboxSupported()) {
return;
}
TestRunner runner;
std::wstring package_name = GenerateRandomPackageName();
ASSERT_EQ(SBOX_ALL_OK,
runner.GetPolicy()->GetConfig()->AddAppContainerProfile(
package_name.c_str(), ACProfileRegistration::kNoFirewall));
EXPECT_EQ(SBOX_ALL_OK, runner.GetPolicy()->GetConfig()->SetTokenLevel(
USER_UNPROTECTED, USER_UNPROTECTED));
EXPECT_EQ(SBOX_TEST_SUCCEEDED,
runner.RunTest(L"CreateTempFileInAppContainer"));
EXPECT_TRUE(AppContainerBase::DeleteNoFirewall(package_name.c_str()));
}
TEST(LowBoxTest, ChildProcessMitigationLowBox) {
if (!features::IsAppContainerSandboxSupported()) {
return;
}
TestRunner runner(JobLevel::kUnprotected, USER_UNPROTECTED, USER_UNPROTECTED);
#if defined(ARCH_CPU_ARM64)
// TODO(crbug.com/41497342) A DPLOG issued when CreateProcess() fails
// conflicts with Csrss lockdown on Win11 ARM64 - so allow Csrss to allow the
// process to run the right exitcode and not an access violation crash.
runner.SetDisableCsrss(false);
#endif // defined(ARCH_CPU_ARM64)
EXPECT_EQ(SBOX_ALL_OK,
runner.GetPolicy()->GetConfig()->SetLowBox(kAppContainerSid));
// Now set the job level to be <= JobLevel::kLimitedUser
// and ensure we can no longer create a child process.
EXPECT_EQ(SBOX_ALL_OK, runner.GetPolicy()->GetConfig()->SetJobLevel(
JobLevel::kLimitedUser, 0));
base::FilePath cmd;
EXPECT_TRUE(base::PathService::Get(base::DIR_SYSTEM, &cmd));
cmd = cmd.Append(L"calc.exe");
std::wstring test_command = L"TestChildProcess \"";
test_command += cmd.value().c_str();
test_command += L"\" false";
EXPECT_EQ(SBOX_TEST_SECOND_ERROR, runner.RunTest(test_command.c_str()));
}
} // namespace sandbox