1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #import <Cocoa/Cocoa.h> 6 #include <dirent.h> 7 8 extern "C" { 9 #include <sandbox.h> 10 } 11 12 #include "base/file_util.h" 13 #include "base/files/file_path.h" 14 #include "base/process/kill.h" 15 #include "base/strings/sys_string_conversions.h" 16 #include "base/strings/utf_string_conversions.h" 17 #include "base/test/multiprocess_test.h" 18 #include "content/common/sandbox_mac.h" 19 #include "testing/gtest/include/gtest/gtest.h" 20 #include "testing/multiprocess_func_list.h" 21 22 namespace { 23 24 static const char* kSandboxAccessPathKey = "sandbox_dir"; 25 static const char* kDeniedSuffix = "_denied"; 26 27 } // namespace 28 29 // Tests need to be in the same namespace as the Sandbox class to be useable 30 // with FRIEND_TEST() declaration. 31 namespace content { 32 33 class MacDirAccessSandboxTest : public base::MultiProcessTest { 34 public: 35 bool CheckSandbox(const std::string& directory_to_try) { 36 setenv(kSandboxAccessPathKey, directory_to_try.c_str(), 1); 37 base::ProcessHandle child_process = SpawnChild("mac_sandbox_path_access", 38 false); 39 if (child_process == base::kNullProcessHandle) { 40 LOG(WARNING) << "SpawnChild failed"; 41 return false; 42 } 43 int code = -1; 44 if (!base::WaitForExitCode(child_process, &code)) { 45 LOG(WARNING) << "base::WaitForExitCode failed"; 46 return false; 47 } 48 return code == 0; 49 } 50 }; 51 52 TEST_F(MacDirAccessSandboxTest, StringEscape) { 53 const struct string_escape_test_data { 54 const char* to_escape; 55 const char* escaped; 56 } string_escape_cases[] = { 57 {"", ""}, 58 {"\b\f\n\r\t\\\"", "\\b\\f\\n\\r\\t\\\\\\\""}, 59 {"/'", "/'"}, 60 {"sandwich", "sandwich"}, 61 {"(sandwich)", "(sandwich)"}, 62 {"^\u2135.\u2136$", "^\\u2135.\\u2136$"}, 63 }; 64 65 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(string_escape_cases); ++i) { 66 std::string out; 67 std::string in(string_escape_cases[i].to_escape); 68 EXPECT_TRUE(Sandbox::QuotePlainString(in, &out)); 69 EXPECT_EQ(string_escape_cases[i].escaped, out); 70 } 71 } 72 73 TEST_F(MacDirAccessSandboxTest, RegexEscape) { 74 const std::string kSandboxEscapeSuffix("(/|$)"); 75 const struct regex_test_data { 76 const wchar_t *to_escape; 77 const char* escaped; 78 } regex_cases[] = { 79 {L"", ""}, 80 {L"/'", "/'"}, // / & ' characters don't need escaping. 81 {L"sandwich", "sandwich"}, 82 {L"(sandwich)", "\\(sandwich\\)"}, 83 }; 84 85 // Check that all characters whose values are smaller than 32 [1F] are 86 // rejected by the regex escaping code. 87 { 88 std::string out; 89 char fail_string[] = {31, 0}; 90 char ok_string[] = {32, 0}; 91 EXPECT_FALSE(Sandbox::QuoteStringForRegex(fail_string, &out)); 92 EXPECT_TRUE(Sandbox::QuoteStringForRegex(ok_string, &out)); 93 } 94 95 // Check that all characters whose values are larger than 126 [7E] are 96 // rejected by the regex escaping code. 97 { 98 std::string out; 99 EXPECT_TRUE(Sandbox::QuoteStringForRegex("}", &out)); // } == 0x7D == 125 100 EXPECT_FALSE(Sandbox::QuoteStringForRegex("~", &out)); // ~ == 0x7E == 126 101 EXPECT_FALSE( 102 Sandbox::QuoteStringForRegex(WideToUTF8(L"^\u2135.\u2136$"), &out)); 103 } 104 105 { 106 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(regex_cases); ++i) { 107 std::string out; 108 std::string in = WideToUTF8(regex_cases[i].to_escape); 109 EXPECT_TRUE(Sandbox::QuoteStringForRegex(in, &out)); 110 std::string expected("^"); 111 expected.append(regex_cases[i].escaped); 112 expected.append(kSandboxEscapeSuffix); 113 EXPECT_EQ(expected, out); 114 } 115 } 116 117 { 118 std::string in_utf8("\\^.$|()[]*+?{}"); 119 std::string expected; 120 expected.push_back('^'); 121 for (size_t i = 0; i < in_utf8.length(); ++i) { 122 expected.push_back('\\'); 123 expected.push_back(in_utf8[i]); 124 } 125 expected.append(kSandboxEscapeSuffix); 126 127 std::string out; 128 EXPECT_TRUE(Sandbox::QuoteStringForRegex(in_utf8, &out)); 129 EXPECT_EQ(expected, out); 130 131 } 132 } 133 134 // A class to handle auto-deleting a directory. 135 class ScopedDirectoryDelete { 136 public: 137 inline void operator()(base::FilePath* x) const { 138 if (x) { 139 base::DeleteFile(*x, true); 140 } 141 } 142 }; 143 144 typedef scoped_ptr_malloc<base::FilePath, ScopedDirectoryDelete> 145 ScopedDirectory; 146 147 TEST_F(MacDirAccessSandboxTest, SandboxAccess) { 148 using base::CreateDirectory; 149 150 base::FilePath tmp_dir; 151 ASSERT_TRUE(base::CreateNewTempDirectory(base::FilePath::StringType(), 152 &tmp_dir)); 153 // This step is important on OS X since the sandbox only understands "real" 154 // paths and the paths CreateNewTempDirectory() returns are empirically in 155 // /var which is a symlink to /private/var . 156 tmp_dir = Sandbox::GetCanonicalSandboxPath(tmp_dir); 157 ScopedDirectory cleanup(&tmp_dir); 158 159 const char* sandbox_dir_cases[] = { 160 "simple_dir_name", 161 "^hello++ $", // Regex. 162 "\\^.$|()[]*+?{}", // All regex characters. 163 }; 164 165 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(sandbox_dir_cases); ++i) { 166 const char* sandbox_dir_name = sandbox_dir_cases[i]; 167 base::FilePath sandbox_dir = tmp_dir.Append(sandbox_dir_name); 168 ASSERT_TRUE(CreateDirectory(sandbox_dir)); 169 ScopedDirectory cleanup_sandbox(&sandbox_dir); 170 171 // Create a sibling directory of the sandbox dir, whose name has sandbox dir 172 // as a substring but to which access is denied. 173 std::string sibling_sandbox_dir_name_denied = 174 std::string(sandbox_dir_cases[i]) + kDeniedSuffix; 175 base::FilePath sibling_sandbox_dir = tmp_dir.Append( 176 sibling_sandbox_dir_name_denied.c_str()); 177 ASSERT_TRUE(CreateDirectory(sibling_sandbox_dir)); 178 ScopedDirectory cleanup_sandbox_sibling(&sibling_sandbox_dir); 179 180 EXPECT_TRUE(CheckSandbox(sandbox_dir.value())); 181 } 182 } 183 184 MULTIPROCESS_TEST_MAIN(mac_sandbox_path_access) { 185 char *sandbox_allowed_dir = getenv(kSandboxAccessPathKey); 186 if (!sandbox_allowed_dir) 187 return -1; 188 189 // Build up a sandbox profile that only allows access to a single directory. 190 NSString *sandbox_profile = 191 @"(version 1)" \ 192 "(deny default)" \ 193 "(allow signal (target self))" \ 194 "(allow sysctl-read)" \ 195 ";ENABLE_DIRECTORY_ACCESS"; 196 197 std::string allowed_dir(sandbox_allowed_dir); 198 Sandbox::SandboxVariableSubstitions substitutions; 199 NSString* allow_dir_sandbox_code = 200 Sandbox::BuildAllowDirectoryAccessSandboxString( 201 base::FilePath(sandbox_allowed_dir), 202 &substitutions); 203 sandbox_profile = [sandbox_profile 204 stringByReplacingOccurrencesOfString:@";ENABLE_DIRECTORY_ACCESS" 205 withString:allow_dir_sandbox_code]; 206 207 std::string final_sandbox_profile_str; 208 if (!Sandbox::PostProcessSandboxProfile(sandbox_profile, 209 [NSArray array], 210 substitutions, 211 &final_sandbox_profile_str)) { 212 LOG(ERROR) << "Call to PostProcessSandboxProfile() failed"; 213 return -1; 214 } 215 216 // Enable Sandbox. 217 char* error_buff = NULL; 218 int error = sandbox_init(final_sandbox_profile_str.c_str(), 0, &error_buff); 219 if (error == -1) { 220 LOG(ERROR) << "Failed to Initialize Sandbox: " << error_buff; 221 return -1; 222 } 223 sandbox_free_error(error_buff); 224 225 // Test Sandbox. 226 227 // We should be able to list the contents of the sandboxed directory. 228 DIR *file_list = NULL; 229 file_list = opendir(sandbox_allowed_dir); 230 if (!file_list) { 231 PLOG(ERROR) << "Sandbox overly restrictive: call to opendir(" 232 << sandbox_allowed_dir 233 << ") failed"; 234 return -1; 235 } 236 closedir(file_list); 237 238 // Test restrictions on accessing files. 239 base::FilePath allowed_dir_path(sandbox_allowed_dir); 240 base::FilePath allowed_file = allowed_dir_path.Append("ok_to_write"); 241 base::FilePath denied_file1 = 242 allowed_dir_path.DirName().Append("cant_access"); 243 244 // Try to write a file who's name has the same prefix as the directory we 245 // allow access to. 246 base::FilePath basename = allowed_dir_path.BaseName(); 247 base::FilePath allowed_parent_dir = allowed_dir_path.DirName(); 248 std::string tricky_filename = basename.value() + "123"; 249 base::FilePath denied_file2 = allowed_parent_dir.Append(tricky_filename); 250 251 if (open(allowed_file.value().c_str(), O_WRONLY | O_CREAT) <= 0) { 252 PLOG(ERROR) << "Sandbox overly restrictive: failed to write (" 253 << allowed_file.value() 254 << ")"; 255 return -1; 256 } 257 258 // Test that we deny access to a sibling of the sandboxed directory whose 259 // name has the sandboxed directory name as a substring. e.g. if the sandbox 260 // directory is /foo/baz then test /foo/baz_denied. 261 { 262 struct stat tmp_stat_info; 263 std::string denied_sibling = 264 std::string(sandbox_allowed_dir) + kDeniedSuffix; 265 if (stat(denied_sibling.c_str(), &tmp_stat_info) > 0) { 266 PLOG(ERROR) << "Sandbox breach: was able to stat (" 267 << denied_sibling.c_str() 268 << ")"; 269 return -1; 270 } 271 } 272 273 // Test that we can stat parent directories of the "allowed" directory. 274 { 275 struct stat tmp_stat_info; 276 if (stat(allowed_parent_dir.value().c_str(), &tmp_stat_info) != 0) { 277 PLOG(ERROR) << "Sandbox overly restrictive: unable to stat (" 278 << allowed_parent_dir.value() 279 << ")"; 280 return -1; 281 } 282 } 283 284 // Test that we can't stat files outside the "allowed" directory. 285 { 286 struct stat tmp_stat_info; 287 if (stat(denied_file1.value().c_str(), &tmp_stat_info) > 0) { 288 PLOG(ERROR) << "Sandbox breach: was able to stat (" 289 << denied_file1.value() 290 << ")"; 291 return -1; 292 } 293 } 294 295 if (open(denied_file1.value().c_str(), O_WRONLY | O_CREAT) > 0) { 296 PLOG(ERROR) << "Sandbox breach: was able to write (" 297 << denied_file1.value() 298 << ")"; 299 return -1; 300 } 301 302 if (open(denied_file2.value().c_str(), O_WRONLY | O_CREAT) > 0) { 303 PLOG(ERROR) << "Sandbox breach: was able to write (" 304 << denied_file2.value() 305 << ")"; 306 return -1; 307 } 308 309 return 0; 310 } 311 312 } // namespace content 313