| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "sandbox/linux/syscall_broker/broker_file_permission.h" |
| |
| #include <fcntl.h> |
| #include <stddef.h> |
| #include <string.h> |
| #include <sys/inotify.h> |
| #include <unistd.h> |
| |
| #include <ostream> |
| #include <string> |
| |
| #include "base/check.h" |
| #include "base/strings/string_util.h" |
| #include "sandbox/linux/syscall_broker/broker_command.h" |
| |
| namespace sandbox { |
| namespace syscall_broker { |
| |
| BrokerFilePermission::BrokerFilePermission(BrokerFilePermission&&) = default; |
| BrokerFilePermission& BrokerFilePermission::operator=(BrokerFilePermission&&) = |
| default; |
| BrokerFilePermission::BrokerFilePermission(const BrokerFilePermission&) = |
| default; |
| BrokerFilePermission& BrokerFilePermission::operator=( |
| const BrokerFilePermission&) = default; |
| |
| BrokerFilePermission::~BrokerFilePermission() = default; |
| |
| bool BrokerFilePermission::ValidatePath(const char* path) { |
| if (!path) { |
| return false; |
| } |
| |
| const size_t len = strlen(path); |
| // No empty paths |
| if (len == 0) |
| return false; |
| // Paths must be absolute and not relative |
| if (path[0] != '/') |
| return false; |
| // No trailing / (but "/" is valid) |
| if (len > 1 && path[len - 1] == '/') |
| return false; |
| // No trailing /.. |
| if (len >= 3 && path[len - 3] == '/' && path[len - 2] == '.' && |
| path[len - 1] == '.') |
| return false; |
| // No /../ anywhere |
| for (size_t i = 0; i < len; i++) { |
| if (path[i] == '/' && (len - i) > 3) { |
| if (path[i + 1] == '.' && path[i + 2] == '.' && path[i + 3] == '/') { |
| return false; |
| } |
| } |
| } |
| return true; |
| } |
| |
| // Async signal safe |
| // Calls std::string::c_str(), strncmp and strlen. All these |
| // methods are async signal safe in common standard libs. |
| // TODO(leecam): remove dependency on std::string |
| bool BrokerFilePermission::MatchPath(const char* requested_filename) const { |
| // Note: This recursive match will allow any path under the allowlisted |
| // path, for any number of directory levels. E.g. if the allowlisted |
| // path is /good/ then the following will be permitted by the policy. |
| // /good/file1 |
| // /good/folder/file2 |
| // /good/folder/folder2/file3 |
| // If an attacker could make 'folder' a symlink to ../../ they would have |
| // access to the entire filesystem. |
| // Allowlisting with multiple depths is useful, e.g /proc/ but |
| // the system needs to ensure symlinks can not be created! |
| // That said if an attacker can convert any of the absolute paths |
| // to a symlink they can control any file on the system also. |
| return recursive() ? base::StartsWith(requested_filename, path_) |
| : requested_filename == path_; |
| } |
| |
| const char* BrokerFilePermission::CheckAccess(const char* requested_filename, |
| int mode) const { |
| // First, check if |mode| is existence, ability to read or ability |
| // to write. We do not support X_OK. |
| if (mode != F_OK && mode & ~(R_OK | W_OK)) |
| return nullptr; |
| |
| if (!ValidatePath(requested_filename)) |
| return nullptr; |
| |
| return CheckAccessInternal(requested_filename, mode); |
| } |
| |
| const char* BrokerFilePermission::CheckAccessInternal( |
| const char* requested_filename, |
| int mode) const { |
| if (!MatchPath(requested_filename)) |
| return nullptr; |
| |
| bool allowed = false; |
| switch (mode) { |
| case F_OK: |
| allowed = allow_read() || allow_write(); |
| break; |
| case R_OK: |
| allowed = allow_read(); |
| break; |
| case W_OK: |
| allowed = allow_write(); |
| break; |
| case R_OK | W_OK: |
| allowed = allow_read() && allow_write(); |
| break; |
| default: |
| break; |
| } |
| if (!allowed) |
| return nullptr; |
| |
| return recursive() ? requested_filename : path_.c_str(); |
| } |
| |
| std::pair<const char*, bool> BrokerFilePermission::CheckOpen( |
| const char* requested_filename, |
| int flags) const { |
| if (!ValidatePath(requested_filename)) |
| return {nullptr, false}; |
| |
| if (!MatchPath(requested_filename)) |
| return {nullptr, false}; |
| |
| // First, check the access mode is valid. |
| const int access_mode = flags & O_ACCMODE; |
| if (access_mode != O_RDONLY && access_mode != O_WRONLY && |
| access_mode != O_RDWR) { |
| return {nullptr, false}; |
| } |
| |
| // Check if read is allowed. |
| if (!allow_read() && (access_mode == O_RDONLY || access_mode == O_RDWR)) { |
| return {nullptr, false}; |
| } |
| |
| // Check if write is allowed. |
| if (!allow_write() && (access_mode == O_WRONLY || access_mode == O_RDWR)) { |
| return {nullptr, false}; |
| } |
| |
| // Check if file creation is allowed. |
| if (!allow_create() && (flags & O_CREAT)) { |
| return {nullptr, false}; |
| } |
| |
| // If this file is to be temporary, ensure it is created, not pre-existing. |
| // See https://crbug.com/415681#c17 |
| if (temporary_only() && (!(flags & O_CREAT) || !(flags & O_EXCL))) { |
| return {nullptr, false}; |
| } |
| |
| // Some flags affect the behavior of the current process. We don't support |
| // them and don't allow them for now. |
| if (flags & kCurrentProcessOpenFlagsMask) { |
| return {nullptr, false}; |
| } |
| |
| // The effect of (O_RDONLY | O_TRUNC) is undefined, and in some cases it |
| // actually truncates, so deny. |
| if (access_mode == O_RDONLY && (flags & O_TRUNC) != 0) { |
| return {nullptr, false}; |
| } |
| |
| // Now check that all the flags are known to us. |
| const int creation_and_status_flags = flags & ~O_ACCMODE; |
| const int known_flags = O_APPEND | O_ASYNC | O_CLOEXEC | O_CREAT | O_DIRECT | |
| O_DIRECTORY | O_EXCL | O_LARGEFILE | O_NOATIME | |
| O_NOCTTY | O_NOFOLLOW | O_NONBLOCK | O_NDELAY | |
| O_SYNC | O_TRUNC; |
| |
| const int unknown_flags = ~known_flags; |
| const bool has_unknown_flags = creation_and_status_flags & unknown_flags; |
| if (has_unknown_flags) { |
| return {nullptr, false}; |
| } |
| |
| return {recursive() ? requested_filename : path_.c_str(), temporary_only()}; |
| } |
| |
| const char* BrokerFilePermission::CheckStatWithIntermediates( |
| const char* requested_filename) const { |
| if (!ValidatePath(requested_filename)) { |
| return nullptr; |
| } |
| |
| // Ability to access implies ability to stat(). |
| const char* ret = CheckAccessInternal(requested_filename, F_OK); |
| if (ret) { |
| return ret; |
| } |
| |
| // Allow stat() on leading directories if have create or stat() permission. |
| if (!(allow_create() || allow_stat_with_intermediates())) { |
| return nullptr; |
| } |
| |
| // |allow_stat_with_intermediates()| can match on the full path, and |
| // |allow_create()| only matches a leading directory. |
| if (!CheckIntermediates( |
| requested_filename, |
| /*can_match_full_path=*/allow_stat_with_intermediates())) { |
| return nullptr; |
| } |
| |
| return requested_filename; |
| } |
| |
| const char* BrokerFilePermission::CheckInotifyAddWatchWithIntermediates( |
| const char* requested_filename, |
| uint32_t mask) const { |
| if (!allow_inotify_add_watch_with_intermediates()) { |
| return nullptr; |
| } |
| |
| if (!ValidatePath(requested_filename)) { |
| return nullptr; |
| } |
| |
| // Allow only this exact mask as it is used by |
| // base/files/file_path_watcher_inotify.cc. |
| if (mask != (IN_ATTRIB | IN_CREATE | IN_DELETE | IN_CLOSE_WRITE | IN_MOVE | |
| IN_ONLYDIR)) { |
| return nullptr; |
| } |
| |
| if (!CheckIntermediates(requested_filename, |
| /*can_match_full_path=*/true)) { |
| return nullptr; |
| } |
| |
| return requested_filename; |
| } |
| |
| bool BrokerFilePermission::CheckIntermediates(const char* requested_filename, |
| bool can_match_full_path) const { |
| // NOTE: ValidatePath proves requested_length != 0 and |requested_filename| is |
| // absolute. |
| size_t requested_length = strlen(requested_filename); |
| CHECK(requested_length); |
| CHECK(requested_filename[0] == '/'); |
| |
| // Special case for root: only one slash, otherwise must have a second |
| // slash in the right spot to avoid substring matches. |
| return (requested_length == 1 && requested_filename[0] == '/') || |
| // If this permission can match the full path, compare directly to the |
| // requested filename. |
| (can_match_full_path && path_ == requested_filename) || |
| // Check whether |requested_filename| matches a leading directory of |
| // |path_|. |
| (requested_length < path_.length() && |
| memcmp(path_.c_str(), requested_filename, requested_length) == 0 && |
| path_.c_str()[requested_length] == '/'); |
| } |
| |
| const char* BrokerFilePermission::GetErrorMessageForTests() { |
| return "Invalid BrokerFilePermission"; |
| } |
| |
| void BrokerFilePermission::DieOnInvalidPermission() { |
| // Must have enough length for a '/' |
| CHECK(path_.length() > 0) << GetErrorMessageForTests(); |
| |
| // Allowlisted paths must be absolute. |
| CHECK(path_[0] == '/') << GetErrorMessageForTests(); |
| |
| // Don't allow temporary creation without create permission. |
| if (temporary_only()) |
| CHECK(allow_create()) << GetErrorMessageForTests(); |
| |
| // Recursive paths must have a trailing slash, absolutes must not (except |
| // root). |
| const char last_char = path_.back(); |
| if (recursive() || path_.length() == 1) |
| CHECK(last_char == '/') << GetErrorMessageForTests(); |
| else |
| CHECK(last_char != '/') << GetErrorMessageForTests(); |
| } |
| |
| BrokerFilePermission::BrokerFilePermission(std::string path, uint64_t flags) |
| : path_(std::move(path)), flags_(flags) { |
| DieOnInvalidPermission(); |
| } |
| |
| BrokerFilePermission::BrokerFilePermission( |
| std::string path, |
| RecursionOption recurse_opt, |
| PersistenceOption persist_opt, |
| ReadPermission read_perm, |
| WritePermission write_perm, |
| CreatePermission create_perm, |
| StatWithIntermediatesPermission stat_perm, |
| InotifyAddWatchWithIntermediatesPermission inotify_perm) |
| : path_(std::move(path)) { |
| flags_[kRecursiveBitPos] = recurse_opt == RecursionOption::kRecursive; |
| flags_[kTemporaryOnlyBitPos] = |
| persist_opt == PersistenceOption::kTemporaryOnly; |
| flags_[kAllowReadBitPos] = read_perm == ReadPermission::kAllowRead; |
| flags_[kAllowWriteBitPos] = write_perm == WritePermission::kAllowWrite; |
| flags_[kAllowCreateBitPos] = create_perm == CreatePermission::kAllowCreate; |
| flags_[kAllowStatWithIntermediatesBitPos] = |
| stat_perm == StatWithIntermediatesPermission::kAllowStatWithIntermediates; |
| flags_[kAllowInotifyAddWatchWithIntermediates] = |
| inotify_perm == InotifyAddWatchWithIntermediatesPermission:: |
| kAllowInotifyAddWatchWithIntermediates; |
| |
| DieOnInvalidPermission(); |
| } |
| |
| } // namespace syscall_broker |
| } // namespace sandbox |