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