| // 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. |
| |
| #include "content/browser/child_process_security_policy_impl.h" |
| |
| #include <tuple> |
| #include <utility> |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/debug/crash_logging.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "build/build_config.h" |
| #include "components/permissions/features.h" |
| #include "content/browser/bad_message.h" |
| #include "content/browser/isolated_origin_util.h" |
| #include "content/browser/process_lock.h" |
| #include "content/browser/renderer_host/render_process_host_impl.h" |
| #include "content/browser/site_info.h" |
| #include "content/browser/site_instance_impl.h" |
| #include "content/browser/url_info.h" |
| #include "content/browser/webui/url_data_manager_backend.h" |
| #include "content/common/content_navigation_policy.h" |
| #include "content/common/features.h" |
| #include "content/public/browser/browser_context.h" |
| #include "content/public/browser/browser_or_resource_context.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/child_process_data.h" |
| #include "content/public/browser/child_process_host.h" |
| #include "content/public/browser/content_browser_client.h" |
| #include "content/public/browser/render_process_host.h" |
| #include "content/public/browser/resource_context.h" |
| #include "content/public/browser/site_instance.h" |
| #include "content/public/browser/site_isolation_policy.h" |
| #include "content/public/browser/storage_partition.h" |
| #include "content/public/common/bindings_policy.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_features.h" |
| #include "content/public/common/url_constants.h" |
| #include "net/base/filename_util.h" |
| #include "net/base/url_util.h" |
| #include "net/net_buildflags.h" |
| #include "services/network/public/cpp/resource_request_body.h" |
| #include "storage/browser/file_system/file_permission_policy.h" |
| #include "storage/browser/file_system/file_system_context.h" |
| #include "storage/browser/file_system/file_system_url.h" |
| #include "storage/browser/file_system/isolated_context.h" |
| #include "storage/common/file_system/file_system_util.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "url/gurl.h" |
| #include "url/url_canon.h" |
| #include "url/url_constants.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| // Used internally only. These bit positions have no relationship to any |
| // underlying OS and can be changed to accommodate finer-grained permissions. |
| enum ChildProcessSecurityPermissions { |
| READ_FILE_PERMISSION = 1 << 0, |
| WRITE_FILE_PERMISSION = 1 << 1, |
| CREATE_NEW_FILE_PERMISSION = 1 << 2, |
| CREATE_OVERWRITE_FILE_PERMISSION = 1 << 3, |
| DELETE_FILE_PERMISSION = 1 << 4, |
| |
| // Used by Media Galleries API |
| COPY_INTO_FILE_PERMISSION = 1 << 5, |
| }; |
| |
| // Used internally only. Bitmasks that are actually used by the Grant* and Can* |
| // methods. These contain one or more ChildProcessSecurityPermissions. |
| enum ChildProcessSecurityGrants { |
| READ_FILE_GRANT = READ_FILE_PERMISSION, |
| WRITE_FILE_GRANT = WRITE_FILE_PERMISSION, |
| |
| CREATE_NEW_FILE_GRANT = CREATE_NEW_FILE_PERMISSION | |
| COPY_INTO_FILE_PERMISSION, |
| |
| CREATE_READ_WRITE_FILE_GRANT = CREATE_NEW_FILE_PERMISSION | |
| CREATE_OVERWRITE_FILE_PERMISSION | |
| READ_FILE_PERMISSION | |
| WRITE_FILE_PERMISSION | |
| COPY_INTO_FILE_PERMISSION | |
| DELETE_FILE_PERMISSION, |
| |
| COPY_INTO_FILE_GRANT = COPY_INTO_FILE_PERMISSION, |
| DELETE_FILE_GRANT = DELETE_FILE_PERMISSION, |
| }; |
| |
| // https://crbug.com/646278 Valid blob URLs should contain canonically |
| // serialized origins. |
| bool IsMalformedBlobUrl(const GURL& url) { |
| if (!url.SchemeIsBlob()) |
| return false; |
| |
| // If the part after blob: survives a roundtrip through url::Origin, then |
| // it's a normal blob URL. |
| std::string canonical_origin = url::Origin::Create(url).Serialize(); |
| canonical_origin.append(1, '/'); |
| if (base::StartsWith(url.GetContent(), canonical_origin, |
| base::CompareCase::INSENSITIVE_ASCII)) |
| return false; |
| |
| // This is a malformed blob URL. |
| return true; |
| } |
| |
| // Helper function that checks to make sure calls on |
| // CanAccessDataForOrigin() are only made on valid threads. |
| // TODO(acolwell): Expand the usage of this check to other |
| // ChildProcessSecurityPolicyImpl methods. |
| bool IsRunningOnExpectedThread() { |
| if (BrowserThread::CurrentlyOn(BrowserThread::IO) || |
| BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| return true; |
| } |
| |
| std::string thread_name(base::PlatformThread::GetName()); |
| |
| // TODO(acolwell): Remove once all tests are updated to properly |
| // identify that they are running on the UI or IO threads. |
| if (thread_name.empty()) |
| return true; |
| |
| LOG(ERROR) << "Running on unexpected thread '" << thread_name << "'"; |
| return false; |
| } |
| |
| base::debug::CrashKeyString* GetRequestedOriginCrashKey() { |
| static auto* requested_origin_key = base::debug::AllocateCrashKeyString( |
| "requested_origin", base::debug::CrashKeySize::Size256); |
| return requested_origin_key; |
| } |
| |
| base::debug::CrashKeyString* GetExpectedProcessLockKey() { |
| static auto* expected_process_lock_key = base::debug::AllocateCrashKeyString( |
| "expected_process_lock", base::debug::CrashKeySize::Size64); |
| return expected_process_lock_key; |
| } |
| |
| base::debug::CrashKeyString* GetKilledProcessOriginLockKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "killed_process_origin_lock", base::debug::CrashKeySize::Size64); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetCanAccessDataFailureReasonKey() { |
| static auto* crash_key = base::debug::AllocateCrashKeyString( |
| "can_access_data_failure_reason", base::debug::CrashKeySize::Size256); |
| return crash_key; |
| } |
| |
| base::debug::CrashKeyString* GetCanAccessDataKeepAliveDurationKey() { |
| static auto* keep_alive_duration_key = base::debug::AllocateCrashKeyString( |
| "keep_alive_duration", base::debug::CrashKeySize::Size256); |
| return keep_alive_duration_key; |
| } |
| |
| base::debug::CrashKeyString* GetCanAccessDataShutdownDelayRefCountKey() { |
| static auto* shutdown_delay_key = base::debug::AllocateCrashKeyString( |
| "shutdown_delay_ref_count", base::debug::CrashKeySize::Size32); |
| return shutdown_delay_key; |
| } |
| |
| base::debug::CrashKeyString* GetCanAccessDataProcessRFHCount() { |
| static auto* process_rfh_count_key = base::debug::AllocateCrashKeyString( |
| "process_rfh_count", base::debug::CrashKeySize::Size32); |
| return process_rfh_count_key; |
| } |
| |
| void LogCanAccessDataForOriginCrashKeys( |
| const std::string& expected_process_lock, |
| const std::string& killed_process_origin_lock, |
| const std::string& requested_origin, |
| const std::string& failure_reason, |
| const std::string& keep_alive_durations, |
| const std::string& shutdown_delay_ref_count, |
| const std::string& process_rfh_count) { |
| base::debug::SetCrashKeyString(GetExpectedProcessLockKey(), |
| expected_process_lock); |
| base::debug::SetCrashKeyString(GetKilledProcessOriginLockKey(), |
| killed_process_origin_lock); |
| base::debug::SetCrashKeyString(GetRequestedOriginCrashKey(), |
| requested_origin); |
| base::debug::SetCrashKeyString(GetCanAccessDataFailureReasonKey(), |
| failure_reason); |
| base::debug::SetCrashKeyString(GetCanAccessDataKeepAliveDurationKey(), |
| keep_alive_durations); |
| base::debug::SetCrashKeyString(GetCanAccessDataShutdownDelayRefCountKey(), |
| shutdown_delay_ref_count); |
| base::debug::SetCrashKeyString(GetCanAccessDataProcessRFHCount(), |
| process_rfh_count); |
| } |
| |
| // Checks whether a lock mismatch should be ignored to allow most visited tiles |
| // to commit in third-party NTP processes. |
| // |
| // TODO(crbug.com/566091): This exception should be removed once these tiles |
| // can be loaded in OOPIFs on the NTP. |
| bool AllowProcessLockMismatchForNTP(const ProcessLock& expected_lock, |
| const ProcessLock& actual_lock) { |
| // First, ensure that the expected lock corresponds to a WebUI site that |
| // does not require its process to be locked. This should only be the case |
| // for sites used to load most visited tiles. |
| const auto& webui_schemes = URLDataManagerBackend::GetWebUISchemes(); |
| if (!base::Contains(webui_schemes, expected_lock.lock_url().scheme())) { |
| return false; |
| } |
| if (GetContentClient()->browser()->DoesWebUIUrlRequireProcessLock( |
| expected_lock.lock_url())) { |
| return false; |
| } |
| |
| // Now, check that the actual lock corresponds to an NTP process (using its |
| // site_url() since this check relies on checking effective URLs for NTPs), |
| // and that the expected lock (based on the URL for which we're doing the |
| // access check) is allowed to stay in that process. This restricts the lock |
| // mismatch to just NTP processes, disallowing most visited tiles from being |
| // embedded on sites in other processes. |
| return GetContentClient()->browser()->ShouldStayInParentProcessForNTP( |
| expected_lock.lock_url(), actual_lock.site_url()); |
| } |
| |
| } // namespace |
| |
| ChildProcessSecurityPolicyImpl::Handle::Handle() |
| : child_id_(ChildProcessHost::kInvalidUniqueID) {} |
| |
| ChildProcessSecurityPolicyImpl::Handle::Handle(int child_id, |
| bool duplicating_handle) |
| : child_id_(child_id) { |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| if (!policy->AddProcessReference(child_id_, duplicating_handle)) |
| child_id_ = ChildProcessHost::kInvalidUniqueID; |
| } |
| |
| ChildProcessSecurityPolicyImpl::Handle::Handle(Handle&& rhs) |
| : child_id_(rhs.child_id_) { |
| rhs.child_id_ = ChildProcessHost::kInvalidUniqueID; |
| } |
| |
| ChildProcessSecurityPolicyImpl::Handle |
| ChildProcessSecurityPolicyImpl::Handle::Duplicate() { |
| return Handle(child_id_, /* duplicating_handle */ true); |
| } |
| |
| ChildProcessSecurityPolicyImpl::Handle::~Handle() { |
| if (child_id_ != ChildProcessHost::kInvalidUniqueID) { |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->RemoveProcessReference(child_id_); |
| } |
| } |
| |
| ChildProcessSecurityPolicyImpl::Handle& ChildProcessSecurityPolicyImpl::Handle:: |
| operator=(Handle&& rhs) { |
| if (child_id_ != ChildProcessHost::kInvalidUniqueID && |
| child_id_ != rhs.child_id_) { |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->RemoveProcessReference(child_id_); |
| } |
| child_id_ = rhs.child_id_; |
| rhs.child_id_ = ChildProcessHost::kInvalidUniqueID; |
| return *this; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::Handle::is_valid() const { |
| return child_id_ != ChildProcessHost::kInvalidUniqueID; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::Handle::CanCommitURL(const GURL& url) { |
| if (child_id_ == ChildProcessHost::kInvalidUniqueID) |
| return false; |
| |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| return policy->CanCommitURL(child_id_, url); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::Handle::CanReadFile( |
| const base::FilePath& file) { |
| if (child_id_ == ChildProcessHost::kInvalidUniqueID) |
| return false; |
| |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| return policy->CanReadFile(child_id_, file); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::Handle::CanReadFileSystemFile( |
| const storage::FileSystemURL& url) { |
| if (child_id_ == ChildProcessHost::kInvalidUniqueID) |
| return false; |
| |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| return policy->CanReadFileSystemFile(child_id_, url); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::Handle::CanAccessDataForOrigin( |
| const url::Origin& origin) { |
| if (child_id_ == ChildProcessHost::kInvalidUniqueID) { |
| LogCanAccessDataForOriginCrashKeys( |
| "(unknown)", "(unknown)", origin.GetDebugString(), "handle_not_valid", |
| "no_keep_alive_durations", "no shutdown delay ref count", |
| "no process rfh count"); |
| return false; |
| } |
| |
| auto* policy = ChildProcessSecurityPolicyImpl::GetInstance(); |
| return policy->CanAccessDataForOrigin(child_id_, origin); |
| } |
| |
| ChildProcessSecurityPolicyImpl::OriginAgentClusterOptInEntry:: |
| OriginAgentClusterOptInEntry( |
| const OriginAgentClusterIsolationState& oac_isolation_state_in, |
| const url::Origin& origin_in) |
| : oac_isolation_state(oac_isolation_state_in), origin(origin_in) {} |
| |
| ChildProcessSecurityPolicyImpl::OriginAgentClusterOptInEntry:: |
| OriginAgentClusterOptInEntry(const OriginAgentClusterOptInEntry&) = default; |
| |
| ChildProcessSecurityPolicyImpl::OriginAgentClusterOptInEntry:: |
| ~OriginAgentClusterOptInEntry() = default; |
| |
| // The SecurityState class is used to maintain per-child process security state |
| // information. |
| class ChildProcessSecurityPolicyImpl::SecurityState { |
| public: |
| typedef std::map<BrowsingInstanceId, OriginAgentClusterIsolationState> |
| BrowsingInstanceInfoMap; |
| |
| explicit SecurityState(BrowserContext* browser_context) |
| : enabled_bindings_(0), |
| can_read_raw_cookies_(false), |
| can_send_midi_(false), |
| can_send_midi_sysex_(false), |
| browser_context_(browser_context), |
| resource_context_(browser_context->GetResourceContext()) { |
| if (!base::FeatureList::IsEnabled( |
| permissions::features::kBlockMidiByDefault)) { |
| can_send_midi_ = true; |
| } |
| } |
| |
| SecurityState(const SecurityState&) = delete; |
| SecurityState& operator=(const SecurityState&) = delete; |
| |
| ~SecurityState() { |
| storage::IsolatedContext* isolated_context = |
| storage::IsolatedContext::GetInstance(); |
| for (auto iter = filesystem_permissions_.begin(); |
| iter != filesystem_permissions_.end(); ++iter) { |
| isolated_context->RemoveReference(iter->first); |
| } |
| UMA_HISTOGRAM_COUNTS_10000( |
| "SiteIsolation.BrowsingInstance.MaxCountPerProcess", |
| max_browsing_instance_count_); |
| } |
| |
| // Grant permission to request and commit URLs with the specified origin. |
| void GrantCommitOrigin(const url::Origin& origin) { |
| if (origin.opaque()) |
| return; |
| origin_map_[origin] = CommitRequestPolicy::kCommitAndRequest; |
| } |
| |
| void GrantRequestOrigin(const url::Origin& origin) { |
| if (origin.opaque()) |
| return; |
| // Anything already in |origin_map_| must have at least request permission |
| // already. In that case, the emplace() below will be a no-op. |
| origin_map_.emplace(origin, CommitRequestPolicy::kRequestOnly); |
| } |
| |
| void GrantCommitScheme(const std::string& scheme) { |
| scheme_map_[scheme] = CommitRequestPolicy::kCommitAndRequest; |
| } |
| |
| void GrantRequestScheme(const std::string& scheme) { |
| // Anything already in |scheme_map_| must have at least request permission |
| // already. In that case, the emplace() below will be a no-op. |
| scheme_map_.emplace(scheme, CommitRequestPolicy::kRequestOnly); |
| } |
| |
| // Grant certain permissions to a file. |
| void GrantPermissionsForFile(const base::FilePath& file, int permissions) { |
| base::FilePath stripped = file.StripTrailingSeparators(); |
| file_permissions_[stripped] |= permissions; |
| } |
| |
| // Grant navigation to a file but not the file:// scheme in general. |
| void GrantRequestOfSpecificFile(const base::FilePath &file) { |
| request_file_set_.insert(file.StripTrailingSeparators()); |
| } |
| |
| // Revokes all permissions granted to a file. |
| void RevokeAllPermissionsForFile(const base::FilePath& file) { |
| base::FilePath stripped = file.StripTrailingSeparators(); |
| file_permissions_.erase(stripped); |
| request_file_set_.erase(stripped); |
| } |
| |
| // Grant certain permissions to a file. |
| void GrantPermissionsForFileSystem(const std::string& filesystem_id, |
| int permissions) { |
| if (!base::Contains(filesystem_permissions_, filesystem_id)) |
| storage::IsolatedContext::GetInstance()->AddReference(filesystem_id); |
| filesystem_permissions_[filesystem_id] |= permissions; |
| } |
| |
| bool HasPermissionsForFileSystem(const std::string& filesystem_id, |
| int permissions) { |
| FileSystemMap::const_iterator it = |
| filesystem_permissions_.find(filesystem_id); |
| if (it == filesystem_permissions_.end()) |
| return false; |
| return (it->second & permissions) == permissions; |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| // Determine if the certain permissions have been granted to a content URI. |
| bool HasPermissionsForContentUri(const base::FilePath& file, |
| int permissions) { |
| DCHECK(!file.empty()); |
| DCHECK(file.IsContentUri()); |
| if (!permissions) |
| return false; |
| base::FilePath file_path = file.StripTrailingSeparators(); |
| FileMap::const_iterator it = file_permissions_.find(file_path); |
| if (it != file_permissions_.end()) |
| return (it->second & permissions) == permissions; |
| return false; |
| } |
| #endif |
| |
| void GrantBindings(int bindings) { |
| enabled_bindings_ |= bindings; |
| } |
| |
| void GrantReadRawCookies() { |
| can_read_raw_cookies_ = true; |
| } |
| |
| void RevokeReadRawCookies() { |
| can_read_raw_cookies_ = false; |
| } |
| |
| void GrantPermissionForMidi() { can_send_midi_ = true; } |
| |
| void GrantPermissionForMidiSysEx() { |
| can_send_midi_ = true; |
| can_send_midi_sysex_ = true; |
| } |
| |
| // Determine whether permission has been granted to commit |url|. |
| bool CanCommitURL(const GURL& url) { |
| DCHECK(!url.SchemeIsBlob() && !url.SchemeIsFileSystem()) |
| << "inner_url extraction should be done already."; |
| // Having permission to a scheme implies permission to all of its URLs. |
| auto scheme_judgment = scheme_map_.find(url.scheme()); |
| if (scheme_judgment != scheme_map_.end() && |
| scheme_judgment->second == CommitRequestPolicy::kCommitAndRequest) { |
| return true; |
| } |
| |
| // Check for permission for specific origin. |
| if (CanCommitOrigin(url::Origin::Create(url))) |
| return true; |
| |
| // file:// URLs may sometimes be more granular, e.g. dragging and dropping a |
| // file from the local filesystem. The child itself may not have been |
| // granted access to the entire file:// scheme, but it should still be |
| // allowed to request the dragged and dropped file. |
| if (url.SchemeIs(url::kFileScheme)) { |
| base::FilePath path; |
| if (net::FileURLToFilePath(url, &path)) |
| return base::Contains(request_file_set_, path); |
| } |
| |
| return false; // Unmentioned schemes are disallowed. |
| } |
| |
| bool CanRequestURL(const GURL& url) { |
| DCHECK(!url.SchemeIsBlob() && !url.SchemeIsFileSystem()) |
| << "inner_url extraction should be done already."; |
| // Having permission to a scheme implies permission to all of its URLs. |
| auto scheme_judgment = scheme_map_.find(url.scheme()); |
| if (scheme_judgment != scheme_map_.end()) |
| return true; |
| |
| if (CanRequestOrigin(url::Origin::Create(url))) |
| return true; |
| |
| // Otherwise, delegate to CanCommitURL. Unmentioned schemes are disallowed. |
| // TODO(dcheng): It would be nice to avoid constructing the origin twice. |
| return CanCommitURL(url); |
| } |
| |
| // Determine if the certain permissions have been granted to a file. |
| bool HasPermissionsForFile(const base::FilePath& file, int permissions) { |
| #if BUILDFLAG(IS_ANDROID) |
| if (file.IsContentUri()) |
| return HasPermissionsForContentUri(file, permissions); |
| #endif |
| if (!permissions || file.empty() || !file.IsAbsolute()) |
| return false; |
| base::FilePath current_path = file.StripTrailingSeparators(); |
| base::FilePath last_path; |
| int skip = 0; |
| while (current_path != last_path) { |
| base::FilePath base_name = current_path.BaseName(); |
| if (base_name.value() == base::FilePath::kParentDirectory) { |
| ++skip; |
| } else if (skip > 0) { |
| if (base_name.value() != base::FilePath::kCurrentDirectory) |
| --skip; |
| } else { |
| FileMap::const_iterator it = file_permissions_.find(current_path); |
| if (it != file_permissions_.end()) |
| return (it->second & permissions) == permissions; |
| } |
| last_path = current_path; |
| current_path = current_path.DirName(); |
| } |
| |
| return false; |
| } |
| |
| void SetProcessLock(const ProcessLock& lock_to_set, |
| const IsolationContext& context, |
| bool is_process_used) { |
| CHECK(!lock_to_set.is_invalid()); |
| CHECK(!process_lock_.is_locked_to_site()); |
| CHECK_NE(SiteInstanceImpl::GetDefaultSiteURL(), lock_to_set.lock_url()); |
| |
| if (process_lock_.is_invalid()) { |
| DCHECK(browsing_instance_info_map_.empty()); |
| CHECK(lock_to_set.allows_any_site() || lock_to_set.is_locked_to_site()); |
| } else { |
| // Verify that we are not trying to update the lock with different |
| // COOP/COEP information. |
| CHECK_EQ(process_lock_.GetWebExposedIsolationInfo(), |
| lock_to_set.GetWebExposedIsolationInfo()); |
| |
| if (process_lock_.allows_any_site()) { |
| // TODO(acolwell): Remove ability to lock to an allows_any_site |
| // lock multiple times. Legacy behavior allows the old "lock to site" |
| // path to generate an "allow_any_site" lock if an empty URL is passed |
| // to SiteInstanceImpl::SetSite(). |
| CHECK(lock_to_set.allows_any_site() || lock_to_set.is_locked_to_site()); |
| |
| // Do not allow a lock to become more strict if the process has already |
| // been used to render any pages. |
| if (lock_to_set.is_locked_to_site()) { |
| CHECK(!is_process_used) |
| << "Cannot lock an already used process to " << lock_to_set; |
| } |
| } else { |
| NOTREACHED() << "Unexpected lock type."; |
| } |
| } |
| |
| process_lock_ = lock_to_set; |
| AddBrowsingInstanceInfo(context); |
| } |
| |
| void AddBrowsingInstanceInfo(const IsolationContext& context) { |
| DCHECK(!context.browsing_instance_id().is_null()); |
| browsing_instance_info_map_.insert( |
| {context.browsing_instance_id(), context.default_isolation_state()}); |
| |
| // Track the maximum number of BrowsingInstances in the process in case |
| // we need to remove delayed cleanup and let the set grow unbounded. |
| // Also track the default isolation state for this BrowsingInstance for |
| // future access checks, since the global default can change over time. |
| if (browsing_instance_info_map_.size() > max_browsing_instance_count_) { |
| max_browsing_instance_count_ = browsing_instance_info_map_.size(); |
| } |
| } |
| |
| const ProcessLock& process_lock() const { return process_lock_; } |
| |
| const BrowsingInstanceInfoMap& browsing_instance_info() { |
| return browsing_instance_info_map_; |
| } |
| |
| void ClearBrowsingInstanceId(const BrowsingInstanceId& id) { |
| browsing_instance_info_map_.erase(id); |
| } |
| |
| bool has_web_ui_bindings() const { |
| return enabled_bindings_ & kWebUIBindingsPolicyMask; |
| } |
| |
| bool can_read_raw_cookies() const { |
| return can_read_raw_cookies_; |
| } |
| |
| bool CanSendMidi() const { |
| if (base::FeatureList::IsEnabled( |
| permissions::features::kBlockMidiByDefault)) { |
| // Ensure the flags are in a consistent state: we can only send SysEx |
| // messages if we can also send non-SysEx messages |
| CHECK(can_send_midi_ || !can_send_midi_sysex_); |
| return can_send_midi_; |
| } else { |
| return true; |
| } |
| } |
| |
| bool CanSendMidiSysEx() const { |
| if (base::FeatureList::IsEnabled( |
| permissions::features::kBlockMidiByDefault)) { |
| // Ensure the flags are in a consistent state: we can only send SysEx |
| // messages if we can also send non-SysEx messages |
| CHECK(can_send_midi_ || !can_send_midi_sysex_); |
| } |
| return can_send_midi_sysex_; |
| } |
| |
| BrowserOrResourceContext GetBrowserOrResourceContext() const { |
| if (BrowserThread::CurrentlyOn(BrowserThread::UI) && browser_context_) |
| return BrowserOrResourceContext(browser_context_); |
| |
| if (BrowserThread::CurrentlyOn(BrowserThread::IO) && resource_context_) |
| return BrowserOrResourceContext(resource_context_); |
| |
| return BrowserOrResourceContext(); |
| } |
| |
| void ClearBrowserContextIfMatches(const BrowserContext* browser_context) { |
| if (browser_context == browser_context_) |
| browser_context_ = nullptr; |
| } |
| |
| private: |
| enum class CommitRequestPolicy { |
| kRequestOnly, |
| kCommitAndRequest, |
| }; |
| |
| bool CanCommitOrigin(const url::Origin& origin) { |
| auto it = origin_map_.find(origin); |
| if (it == origin_map_.end()) |
| return false; |
| return it->second == CommitRequestPolicy::kCommitAndRequest; |
| } |
| |
| bool CanRequestOrigin(const url::Origin& origin) { |
| // Anything already in |origin_map_| must have at least request permissions |
| // already. |
| return origin_map_.find(origin) != origin_map_.end(); |
| } |
| |
| typedef std::map<std::string, CommitRequestPolicy> SchemeMap; |
| typedef std::map<url::Origin, CommitRequestPolicy> OriginMap; |
| |
| typedef int FilePermissionFlags; // bit-set of base::File::Flags |
| typedef std::map<base::FilePath, FilePermissionFlags> FileMap; |
| typedef std::map<std::string, FilePermissionFlags> FileSystemMap; |
| typedef std::set<base::FilePath> FileSet; |
| |
| // Maps URL schemes to commit/request policies the child process has been |
| // granted. There is no provision for revoking. |
| SchemeMap scheme_map_; |
| |
| // The map of URL origins to commit/request policies the child process has |
| // been granted. There is no provision for revoking. |
| OriginMap origin_map_; |
| |
| // The set of files the child process is permited to upload to the web. |
| FileMap file_permissions_; |
| |
| // The set of files the child process is permitted to load. |
| FileSet request_file_set_; |
| |
| int enabled_bindings_; |
| |
| bool can_read_raw_cookies_; |
| |
| bool can_send_midi_; |
| |
| bool can_send_midi_sysex_; |
| |
| ProcessLock process_lock_; |
| |
| // A map containing the IDs of all BrowsingInstances with documents in this |
| // process, along with their default isolation states. Empty when |
| // |process_lock_| is invalid, or if all BrowsingInstances in the |
| // SecurityState have been destroyed. |
| // |
| // After a process is locked, it might be reused by navigations from frames |
| // in other BrowsingInstances, e.g., when we're over process limit and when |
| // those navigations utilize the same process lock. This set tracks all the |
| // BrowsingInstances that share this process. |
| // |
| // This is needed for security checks on the IO thread, where we only know |
| // the process ID and need to compute the expected origin lock, which |
| // requires knowing the set of applicable isolated origins in each respective |
| // BrowsingInstance. |
| BrowsingInstanceInfoMap browsing_instance_info_map_; |
| |
| // The maximum number of BrowsingInstances that have been in this |
| // SecurityState's RenderProcessHost, for metrics. |
| unsigned max_browsing_instance_count_ = 0; |
| |
| // The set of isolated filesystems the child process is permitted to access. |
| FileSystemMap filesystem_permissions_; |
| |
| raw_ptr<BrowserContext> browser_context_; |
| raw_ptr<ResourceContext, DanglingAcrossTasks> resource_context_; |
| }; |
| |
| // IsolatedOriginEntry implementation. |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry( |
| const url::Origin& origin, |
| bool applies_to_future_browsing_instances, |
| BrowsingInstanceId browsing_instance_id, |
| BrowserContext* browser_context, |
| ResourceContext* resource_context, |
| bool isolate_all_subdomains, |
| IsolatedOriginSource source) |
| : origin_(origin), |
| applies_to_future_browsing_instances_( |
| applies_to_future_browsing_instances), |
| browsing_instance_id_(browsing_instance_id), |
| browser_context_(browser_context), |
| resource_context_(resource_context), |
| isolate_all_subdomains_(isolate_all_subdomains), |
| source_(source) { |
| // If there is a BrowserContext, there must also be a ResourceContext |
| // associated with this entry. |
| DCHECK_EQ(!browser_context, !resource_context); |
| } |
| |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry( |
| const IsolatedOriginEntry& other) = default; |
| |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry& |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::operator=( |
| const IsolatedOriginEntry& other) = default; |
| |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::IsolatedOriginEntry( |
| IsolatedOriginEntry&& other) = default; |
| |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry& |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::operator=( |
| IsolatedOriginEntry&& other) = default; |
| |
| ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::~IsolatedOriginEntry() = |
| default; |
| |
| bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry:: |
| AppliesToAllBrowserContexts() const { |
| return !browser_context_; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry::MatchesProfile( |
| const BrowserOrResourceContext& browser_or_resource_context) const { |
| DCHECK(IsRunningOnExpectedThread()); |
| |
| // Globally isolated origins aren't associated with any particular profile |
| // and should apply to all profiles. |
| if (AppliesToAllBrowserContexts()) |
| return true; |
| |
| if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| return browser_context_ == browser_or_resource_context.ToBrowserContext(); |
| } else if (BrowserThread::CurrentlyOn(BrowserThread::IO)) { |
| return resource_context_ == browser_or_resource_context.ToResourceContext(); |
| } |
| NOTREACHED(); |
| return false; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsolatedOriginEntry:: |
| MatchesBrowsingInstance(BrowsingInstanceId browsing_instance_id) const { |
| if (applies_to_future_browsing_instances_) |
| return browsing_instance_id_ <= browsing_instance_id; |
| |
| return browsing_instance_id_ == browsing_instance_id; |
| } |
| |
| // Make sure BrowsingInstance state is cleaned up after the max amount of time |
| // RenderProcessHost might stick around for various IncrementKeepAliveRefCount |
| // calls. For now, track that as the KeepAliveHandleFactory timeout (the current |
| // longest value) plus the unload timeout, with a bit of an extra margin. |
| // // TODO(wjmaclean): Refactor IncrementKeepAliveRefCount to track how much |
| // time is needed rather than leaving the interval open ended, so that we can |
| // enforce a max delay here and in RenderProcessHost. https://crbug.com/1181838 |
| ChildProcessSecurityPolicyImpl::ChildProcessSecurityPolicyImpl() |
| : browsing_instance_cleanup_delay_( |
| RenderProcessHostImpl::kKeepAliveHandleFactoryTimeout + |
| base::Seconds(2)) { |
| // We know about these schemes and believe them to be safe. |
| RegisterWebSafeScheme(url::kHttpScheme); |
| RegisterWebSafeScheme(url::kHttpsScheme); |
| #if BUILDFLAG(ENABLE_WEBSOCKETS) |
| RegisterWebSafeScheme(url::kWsScheme); |
| RegisterWebSafeScheme(url::kWssScheme); |
| #endif // BUILDFLAG(ENABLE_WEBSOCKETS) |
| RegisterWebSafeScheme(url::kDataScheme); |
| |
| // TODO(nick): https://crbug.com/651534 blob: and filesystem: schemes embed |
| // other origins, so we should not treat them as web safe. Remove callers of |
| // IsWebSafeScheme(), and then eliminate the next two lines. |
| RegisterWebSafeScheme(url::kBlobScheme); |
| RegisterWebSafeScheme(url::kFileSystemScheme); |
| |
| // We know about the following pseudo schemes and treat them specially. |
| RegisterPseudoScheme(url::kAboutScheme); |
| RegisterPseudoScheme(url::kJavaScriptScheme); |
| RegisterPseudoScheme(kViewSourceScheme); |
| RegisterPseudoScheme(kGoogleChromeScheme); |
| } |
| |
| ChildProcessSecurityPolicyImpl::~ChildProcessSecurityPolicyImpl() { |
| } |
| |
| // static |
| ChildProcessSecurityPolicy* ChildProcessSecurityPolicy::GetInstance() { |
| return ChildProcessSecurityPolicyImpl::GetInstance(); |
| } |
| |
| ChildProcessSecurityPolicyImpl* ChildProcessSecurityPolicyImpl::GetInstance() { |
| return base::Singleton<ChildProcessSecurityPolicyImpl>::get(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::Add(int child_id, |
| BrowserContext* browser_context) { |
| DCHECK(browser_context); |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK_NE(child_id, ChildProcessHost::kInvalidUniqueID); |
| base::AutoLock lock(lock_); |
| if (security_state_.find(child_id) != security_state_.end()) { |
| NOTREACHED() << "Add child process at most once."; |
| return; |
| } |
| |
| security_state_[child_id] = std::make_unique<SecurityState>(browser_context); |
| CHECK(AddProcessReferenceLocked(child_id, /* duplicating_handle */ false)); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddForTesting( |
| int child_id, |
| BrowserContext* browser_context) { |
| Add(child_id, browser_context); |
| LockProcess( |
| IsolationContext(BrowsingInstanceId(1), browser_context, |
| /*is_guest=*/false, /*is_fenced=*/false, |
| OriginAgentClusterIsolationState::CreateNonIsolated()), |
| child_id, /*is_process_used=*/false, |
| ProcessLock::CreateAllowAnySite( |
| StoragePartitionConfig::CreateDefault(browser_context), |
| WebExposedIsolationInfo::CreateNonIsolated())); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::Remove(int child_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK_NE(child_id, ChildProcessHost::kInvalidUniqueID); |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| // Moving the existing SecurityState object into a pending map so |
| // that we can preserve permission state and avoid mutations to this |
| // state after Remove() has been called. |
| pending_remove_state_[child_id] = std::move(state->second); |
| security_state_.erase(child_id); |
| |
| RemoveProcessReferenceLocked(child_id); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RegisterWebSafeScheme( |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme)) |
| << "Add schemes at most once."; |
| DCHECK_EQ(0U, pseudo_schemes_.count(scheme)) |
| << "Web-safe implies not pseudo."; |
| |
| schemes_okay_to_request_in_any_process_.insert(scheme); |
| schemes_okay_to_commit_in_any_process_.insert(scheme); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RegisterWebSafeIsolatedScheme( |
| const std::string& scheme, |
| bool always_allow_in_origin_headers) { |
| base::AutoLock lock(lock_); |
| DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme)) |
| << "Add schemes at most once."; |
| DCHECK_EQ(0U, pseudo_schemes_.count(scheme)) |
| << "Web-safe implies not pseudo."; |
| |
| schemes_okay_to_request_in_any_process_.insert(scheme); |
| if (always_allow_in_origin_headers) |
| schemes_okay_to_appear_as_origin_headers_.insert(scheme); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsWebSafeScheme( |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| |
| return base::Contains(schemes_okay_to_request_in_any_process_, scheme); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RegisterPseudoScheme( |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| DCHECK_EQ(0U, pseudo_schemes_.count(scheme)) << "Add schemes at most once."; |
| DCHECK_EQ(0U, schemes_okay_to_request_in_any_process_.count(scheme)) |
| << "Pseudo implies not web-safe."; |
| DCHECK_EQ(0U, schemes_okay_to_commit_in_any_process_.count(scheme)) |
| << "Pseudo implies not web-safe."; |
| |
| pseudo_schemes_.insert(scheme); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsPseudoScheme( |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| |
| return base::Contains(pseudo_schemes_, scheme); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::ClearRegisteredSchemeForTesting( |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| schemes_okay_to_request_in_any_process_.erase(scheme); |
| schemes_okay_to_commit_in_any_process_.erase(scheme); |
| pseudo_schemes_.erase(scheme); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCommitURL(int child_id, |
| const GURL& url) { |
| // Can't grant the capability to commit invalid URLs. |
| if (!url.is_valid()) |
| return; |
| |
| // Can't grant the capability to commit pseudo schemes. |
| if (IsPseudoScheme(url.scheme())) |
| return; |
| |
| url::Origin origin = url::Origin::Create(url); |
| |
| // Blob and filesystem URLs require special treatment; grant access to the |
| // inner origin they embed instead. |
| // TODO(dcheng): Can this logic be simplified to just derive an origin up |
| // front and use that? That probably requires fixing GURL canonicalization of |
| // blob URLs though. For now, be consistent with how CanRequestURL and |
| // CanCommitURL normalize. |
| if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) { |
| if (IsMalformedBlobUrl(url)) |
| return; |
| |
| GrantCommitURL(child_id, GURL(origin.Serialize())); |
| } |
| |
| // TODO(dcheng): In the future, URLs with opaque origins would ideally carry |
| // around an origin with them, so we wouldn't need to grant commit access to |
| // the entire scheme. |
| if (!origin.opaque()) |
| GrantCommitOrigin(child_id, origin); |
| |
| // The scheme has already been whitelisted for every child process, so no need |
| // to do anything else. |
| if (IsWebSafeScheme(url.scheme())) |
| return; |
| |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| if (origin.opaque()) { |
| // If it's impossible to grant commit rights to just the origin (among other |
| // things, URLs with non-standard schemes will be treated as opaque |
| // origins), then grant access to commit all URLs of that scheme. |
| state->second->GrantCommitScheme(url.scheme()); |
| } else { |
| // When the child process has been commanded to request this scheme, grant |
| // it the capability to request all URLs of that scheme. |
| state->second->GrantRequestScheme(url.scheme()); |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantRequestSpecificFileURL( |
| int child_id, |
| const GURL& url) { |
| if (!url.SchemeIs(url::kFileScheme)) |
| return; |
| |
| { |
| base::AutoLock lock(lock_); |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| // When the child process has been commanded to request a file:// URL, |
| // then we grant it the capability for that URL only. |
| base::FilePath path; |
| if (net::FileURLToFilePath(url, &path)) |
| state->second->GrantRequestOfSpecificFile(path); |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantReadFile(int child_id, |
| const base::FilePath& file) { |
| GrantPermissionsForFile(child_id, file, READ_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCreateReadWriteFile( |
| int child_id, const base::FilePath& file) { |
| GrantPermissionsForFile(child_id, file, CREATE_READ_WRITE_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCopyInto(int child_id, |
| const base::FilePath& dir) { |
| GrantPermissionsForFile(child_id, dir, COPY_INTO_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantDeleteFrom( |
| int child_id, const base::FilePath& dir) { |
| GrantPermissionsForFile(child_id, dir, DELETE_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantPermissionsForFile( |
| int child_id, const base::FilePath& file, int permissions) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantPermissionsForFile(file, permissions); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RevokeAllPermissionsForFile( |
| int child_id, const base::FilePath& file) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->RevokeAllPermissionsForFile(file); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantReadFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem(child_id, filesystem_id, READ_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantWriteFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem(child_id, filesystem_id, WRITE_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCreateFileForFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem(child_id, filesystem_id, CREATE_NEW_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCreateReadWriteFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem( |
| child_id, filesystem_id, CREATE_READ_WRITE_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCopyIntoFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem(child_id, filesystem_id, COPY_INTO_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantDeleteFromFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| GrantPermissionsForFileSystem(child_id, filesystem_id, DELETE_FILE_GRANT); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantSendMidiMessage(int child_id) { |
| if (base::FeatureList::IsEnabled( |
| permissions::features::kBlockMidiByDefault)) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) { |
| return; |
| } |
| |
| state->second->GrantPermissionForMidi(); |
| } |
| return; |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantSendMidiSysExMessage(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantPermissionForMidiSysEx(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantCommitOrigin( |
| int child_id, |
| const url::Origin& origin) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantCommitOrigin(origin); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantRequestOrigin( |
| int child_id, |
| const url::Origin& origin) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantRequestOrigin(origin); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantRequestScheme( |
| int child_id, |
| const std::string& scheme) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantRequestScheme(scheme); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantWebUIBindings(int child_id, |
| int bindings) { |
| // Only WebUI bindings should come through here. |
| CHECK(bindings & kWebUIBindingsPolicyMask); |
| CHECK_EQ(0, bindings & ~kWebUIBindingsPolicyMask); |
| |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantBindings(bindings); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantReadRawCookies(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->GrantReadRawCookies(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RevokeReadRawCookies(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| |
| state->second->RevokeReadRawCookies(); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanRequestURL( |
| int child_id, const GURL& url) { |
| if (!url.is_valid()) |
| return false; // Can't request invalid URLs. |
| |
| const std::string& scheme = url.scheme(); |
| |
| // Every child process can request <about:blank>, <about:blank?foo>, |
| // <about:blank/#foo> and <about:srcdoc>. |
| // |
| // URLs like <about:version>, <about:crash>, <view-source:...> shouldn't be |
| // requestable by any child process. Also, this case covers |
| // <javascript:...>, which should be handled internally by the process and |
| // not kicked up to the browser. |
| // TODO(dcheng): Figure out why this check is different from CanCommitURL, |
| // which checks for direct equality with kAboutBlankURL. |
| if (IsPseudoScheme(scheme)) |
| return url.IsAboutBlank() || url.IsAboutSrcdoc(); |
| |
| // Blob and filesystem URLs require special treatment; validate the inner |
| // origin they embed. |
| if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) { |
| if (IsMalformedBlobUrl(url)) |
| return false; |
| |
| url::Origin origin = url::Origin::Create(url); |
| return origin.opaque() || CanRequestURL(child_id, GURL(origin.Serialize())); |
| } |
| |
| if (IsWebSafeScheme(scheme)) |
| return true; |
| |
| { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return false; |
| |
| // Otherwise, we consult the child process's security state to see if it is |
| // allowed to request the URL. |
| if (state->second->CanRequestURL(url)) |
| return true; |
| } |
| |
| // If |url| has WebUI scheme, the process must usually be locked, unless |
| // running in single-process mode. Since this is a check whether the process |
| // can request |url|, the check must operate based on scheme because one WebUI |
| // should be able to request subresources from another WebUI of the same |
| // scheme. |
| const auto& webui_schemes = URLDataManagerBackend::GetWebUISchemes(); |
| if (!RenderProcessHost::run_renderer_in_process() && |
| base::Contains(webui_schemes, url.scheme())) { |
| bool should_be_locked = |
| GetContentClient()->browser()->DoesWebUIUrlRequireProcessLock(url); |
| if (should_be_locked) { |
| const ProcessLock lock = GetProcessLock(child_id); |
| if (!lock.is_locked_to_site() || !lock.matches_scheme(url.scheme())) |
| return false; |
| } |
| } |
| |
| // Also allow URLs destined for ShellExecute and not the browser itself. |
| return !GetContentClient()->browser()->IsHandledURL(url); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanRedirectToURL(const GURL& url) { |
| if (!url.is_valid()) |
| return false; // Can't redirect to invalid URLs. |
| |
| const std::string& scheme = url.scheme(); |
| |
| // Can't redirect to error pages. |
| if (scheme == kChromeErrorScheme) |
| return false; |
| |
| if (IsPseudoScheme(scheme)) { |
| // Redirects to a pseudo scheme (about, javascript, view-source, ...) are |
| // not allowed. An exception is made for <about:blank> and its variations. |
| return url.IsAboutBlank(); |
| } |
| |
| // Note about redirects and special URLs: |
| // * data-url: Blocked by net::DataProtocolHandler::IsSafeRedirectTarget(). |
| // * filesystem-url: Blocked by |
| // storage::FilesystemProtocolHandler::IsSafeRedirectTarget(). |
| // Depending on their inner origins and if the request is browser-initiated or |
| // renderer-initiated, blob-urls might get blocked by CanCommitURL or in |
| // DocumentLoader::RedirectReceived. If not blocked, a 'file not found' |
| // response will be generated in net::BlobURLRequestJob::DidStart(). |
| |
| return true; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCommitURL(int child_id, |
| const GURL& url) { |
| if (!url.is_valid()) |
| return false; // Can't commit invalid URLs. |
| |
| const std::string& scheme = url.scheme(); |
| |
| // Of all the pseudo schemes, only about:blank and about:srcdoc are allowed to |
| // commit. |
| if (IsPseudoScheme(scheme)) |
| return url.IsAboutBlank() || url.IsAboutSrcdoc(); |
| |
| // Blob and filesystem URLs require special treatment; validate the inner |
| // origin they embed. |
| if (url.SchemeIsBlob() || url.SchemeIsFileSystem()) { |
| if (IsMalformedBlobUrl(url)) |
| return false; |
| |
| url::Origin origin = url::Origin::Create(url); |
| return origin.opaque() || CanCommitURL(child_id, GURL(origin.Serialize())); |
| } |
| |
| // With site isolation, a URL from a site may only be committed in a process |
| // dedicated to that site. This check will ensure that |url| can't commit if |
| // the process is locked to a different site. |
| if (!CanAccessDataForMaybeOpaqueOrigin( |
| child_id, url, false /* url_is_precursor_of_opaque_origin */)) |
| return false; |
| |
| { |
| base::AutoLock lock(lock_); |
| |
| // Most schemes can commit in any process. Note that we check |
| // schemes_okay_to_commit_in_any_process_ here, which is stricter than |
| // IsWebSafeScheme(). |
| // |
| // TODO(creis, nick): https://crbug.com/515309: The line below does not |
| // enforce that http pages cannot commit in an extension process. |
| if (base::Contains(schemes_okay_to_commit_in_any_process_, scheme)) |
| return true; |
| |
| auto* state = GetSecurityState(child_id); |
| if (!state) |
| return false; |
| |
| // Otherwise, we consult the child process's security state to see if it is |
| // allowed to commit the URL. |
| return state->CanCommitURL(url); |
| } |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadFile(int child_id, |
| const base::FilePath& file) { |
| return HasPermissionsForFile(child_id, file, READ_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadAllFiles( |
| int child_id, |
| const std::vector<base::FilePath>& files) { |
| return base::ranges::all_of(files, |
| [this, child_id](const base::FilePath& file) { |
| return CanReadFile(child_id, file); |
| }); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadRequestBody( |
| int child_id, |
| const storage::FileSystemContext* file_system_context, |
| const scoped_refptr<network::ResourceRequestBody>& body) { |
| if (!body) |
| return true; |
| |
| for (const network::DataElement& element : *body->elements()) { |
| switch (element.type()) { |
| case network::DataElement::Tag::kFile: |
| if (!CanReadFile(child_id, |
| element.As<network::DataElementFile>().path())) |
| return false; |
| break; |
| |
| case network::DataElement::Tag::kBytes: |
| // Data is self-contained within |body| - no need to check access. |
| break; |
| |
| case network::DataElement::Tag::kDataPipe: |
| // Data is self-contained within |body| - no need to check access. |
| break; |
| |
| default: |
| // Fail safe - deny access. |
| NOTREACHED(); |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadRequestBody( |
| RenderProcessHost* process, |
| const scoped_refptr<network::ResourceRequestBody>& body) { |
| CHECK(process); |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| return CanReadRequestBody( |
| process->GetID(), process->GetStoragePartition()->GetFileSystemContext(), |
| body); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCreateReadWriteFile( |
| int child_id, |
| const base::FilePath& file) { |
| return HasPermissionsForFile(child_id, file, CREATE_READ_WRITE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| return HasPermissionsForFileSystem(child_id, filesystem_id, READ_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadWriteFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| return HasPermissionsForFileSystem(child_id, filesystem_id, |
| READ_FILE_GRANT | WRITE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCopyIntoFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| return HasPermissionsForFileSystem(child_id, filesystem_id, |
| COPY_INTO_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanDeleteFromFileSystem( |
| int child_id, const std::string& filesystem_id) { |
| return HasPermissionsForFileSystem(child_id, filesystem_id, |
| DELETE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::HasPermissionsForFile( |
| int child_id, const base::FilePath& file, int permissions) { |
| base::AutoLock lock(lock_); |
| return ChildProcessHasPermissionsForFile(child_id, file, permissions); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url, |
| int permissions) { |
| if (!filesystem_url.is_valid()) |
| return false; |
| |
| if (filesystem_url.path().ReferencesParent()) |
| return false; |
| |
| // Any write access is disallowed on the root path. |
| if (storage::VirtualPath::IsRootPath(filesystem_url.path()) && |
| (permissions & ~READ_FILE_GRANT)) { |
| return false; |
| } |
| |
| if (filesystem_url.mount_type() == storage::kFileSystemTypeIsolated) { |
| // When Isolated filesystems is overlayed on top of another filesystem, |
| // its per-filesystem permission overrides the underlying filesystem |
| // permissions). |
| return HasPermissionsForFileSystem( |
| child_id, filesystem_url.mount_filesystem_id(), permissions); |
| } |
| |
| // If |filesystem_url.origin()| is not committable in this process, then this |
| // page should not be able to place content in that origin via the filesystem |
| // API either. |
| // TODO(lukasza): Audit whether CanAccessDataForOrigin can be used directly |
| // here. |
| if (!CanCommitURL(child_id, filesystem_url.origin().GetURL())) |
| return false; |
| |
| int found_permissions = 0; |
| { |
| base::AutoLock lock(lock_); |
| auto found = file_system_policy_map_.find(filesystem_url.type()); |
| if (found == file_system_policy_map_.end()) |
| return false; |
| found_permissions = found->second; |
| } |
| |
| if ((found_permissions & storage::FILE_PERMISSION_READ_ONLY) && |
| permissions & ~READ_FILE_GRANT) { |
| return false; |
| } |
| |
| // Note that HasPermissionsForFile (called below) will internally acquire the |
| // |lock_|, therefore the |lock_| has to be released before the call (since |
| // base::Lock is not reentrant). |
| if (found_permissions & storage::FILE_PERMISSION_USE_FILE_PERMISSION) |
| return HasPermissionsForFile(child_id, filesystem_url.path(), permissions); |
| |
| if (found_permissions & storage::FILE_PERMISSION_SANDBOX) |
| return true; |
| |
| return false; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| READ_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanWriteFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| WRITE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCreateFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| CREATE_NEW_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCreateReadWriteFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| CREATE_READ_WRITE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCopyIntoFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| COPY_INTO_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanDeleteFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& filesystem_url) { |
| return HasPermissionsForFileSystemFile(child_id, filesystem_url, |
| DELETE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanMoveFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& src_url, |
| const storage::FileSystemURL& dest_url) { |
| return HasPermissionsForFileSystemFile(child_id, dest_url, |
| CREATE_NEW_FILE_GRANT) && |
| HasPermissionsForFileSystemFile(child_id, src_url, READ_FILE_GRANT) && |
| HasPermissionsForFileSystemFile(child_id, src_url, DELETE_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanCopyFileSystemFile( |
| int child_id, |
| const storage::FileSystemURL& src_url, |
| const storage::FileSystemURL& dest_url) { |
| return HasPermissionsForFileSystemFile(child_id, src_url, READ_FILE_GRANT) && |
| HasPermissionsForFileSystemFile(child_id, dest_url, |
| COPY_INTO_FILE_GRANT); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::HasWebUIBindings(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return false; |
| |
| return state->second->has_web_ui_bindings(); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanReadRawCookies(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return false; |
| |
| return state->second->can_read_raw_cookies(); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::ChildProcessHasPermissionsForFile( |
| int child_id, const base::FilePath& file, int permissions) { |
| auto* state = GetSecurityState(child_id); |
| if (!state) |
| return false; |
| return state->HasPermissionsForFile(file, permissions); |
| } |
| |
| size_t ChildProcessSecurityPolicyImpl::BrowsingInstanceIdCountForTesting( |
| int child_id) { |
| base::AutoLock lock(lock_); |
| SecurityState* security_state = GetSecurityState(child_id); |
| if (security_state) |
| return security_state->browsing_instance_info().size(); |
| return 0; |
| } |
| |
| CanCommitStatus ChildProcessSecurityPolicyImpl::CanCommitOriginAndUrl( |
| int child_id, |
| const IsolationContext& isolation_context, |
| const UrlInfo& url_info) { |
| DCHECK(url_info.origin.has_value()); |
| const url::Origin url_origin = |
| url::Origin::Resolve(url_info.url, *url_info.origin); |
| if (!CanAccessDataForOrigin(child_id, url_origin)) { |
| // Check for special cases, like blob:null/ and data: URLs, where the |
| // origin does not contain information to match against the process lock, |
| // but using the whole URL can result in a process lock match. Note that |
| // the origin being committed in `url_info.origin` will not actually be |
| // used when computing `expected_process_lock` below in many cases; see |
| // https://crbug.com/1320402. |
| const auto expected_process_lock = |
| ProcessLock::Create(isolation_context, url_info); |
| const ProcessLock& actual_process_lock = GetProcessLock(child_id); |
| if (actual_process_lock == expected_process_lock) |
| return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL; |
| |
| return CanCommitStatus::CANNOT_COMMIT_URL; |
| } |
| |
| if (!CanAccessDataForOrigin(child_id, *url_info.origin)) |
| return CanCommitStatus::CANNOT_COMMIT_ORIGIN; |
| |
| // Ensure that the origin derived from |url| is consistent with |origin|. |
| // Note: We can't use origin.IsSameOriginWith() here because opaque origins |
| // with precursors may have different nonce values. |
| const auto url_tuple_or_precursor_tuple = |
| url_origin.GetTupleOrPrecursorTupleIfOpaque(); |
| const auto origin_tuple_or_precursor_tuple = |
| url_info.origin->GetTupleOrPrecursorTupleIfOpaque(); |
| |
| if (url_tuple_or_precursor_tuple.IsValid() && |
| origin_tuple_or_precursor_tuple.IsValid() && |
| origin_tuple_or_precursor_tuple != url_tuple_or_precursor_tuple) { |
| // Allow a WebView specific exception for origins that have a data scheme. |
| // WebView converts data: URLs into non-opaque data:// origins which is |
| // different than what all other builds do. This causes the consistency |
| // check to fail because we try to compare a data:// origin with an opaque |
| // origin that contains precursor info. |
| if (url_tuple_or_precursor_tuple.scheme() == url::kDataScheme && |
| url::AllowNonStandardSchemesForAndroidWebView()) { |
| return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL; |
| } |
| |
| return CanCommitStatus::CANNOT_COMMIT_ORIGIN; |
| } |
| |
| return CanCommitStatus::CAN_COMMIT_ORIGIN_AND_URL; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanAccessDataForOrigin( |
| int child_id, |
| const url::Origin& origin) { |
| if (ShouldRestrictCanAccessDataForOriginToUIThread()) { |
| // Ensure this is only called on the UI thread, which is the only thread |
| // with sufficient information to do the full set of checks. |
| CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| } else { |
| // For legacy cases, this may be called on multiple threads. |
| DCHECK(IsRunningOnExpectedThread()); |
| } |
| |
| GURL url_to_check; |
| if (origin.opaque()) { |
| auto precursor_tuple = origin.GetTupleOrPrecursorTupleIfOpaque(); |
| if (!precursor_tuple.IsValid()) { |
| // Allow opaque origins w/o precursors (if the security state exists). |
| // TODO(acolwell): Investigate all cases that trigger this path (e.g., |
| // browser-initiated navigations to data: URLs) and fix them so we have |
| // precursor information (or the process lock is compatible with a missing |
| // precursor). Remove this logic once that has been completed. |
| base::AutoLock lock(lock_); |
| SecurityState* security_state = GetSecurityState(child_id); |
| return !!security_state; |
| } else { |
| url_to_check = precursor_tuple.GetURL(); |
| } |
| } else { |
| url_to_check = origin.GetURL(); |
| } |
| bool success = CanAccessDataForMaybeOpaqueOrigin(child_id, url_to_check, |
| origin.opaque()); |
| if (success) |
| return true; |
| |
| // Note: LogCanAccessDataForOriginCrashKeys() is called in the |
| // CanAccessDataForOrigin() call above. The code below overrides the origin |
| // crash key set in that call with data from |origin| because it provides |
| // more accurate information than the origin derived from |url_to_check|. |
| auto* requested_origin_key = GetRequestedOriginCrashKey(); |
| base::debug::SetCrashKeyString(requested_origin_key, origin.GetDebugString()); |
| return false; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanAccessDataForMaybeOpaqueOrigin( |
| int child_id, |
| const GURL& url, |
| bool url_is_precursor_of_opaque_origin) { |
| if (ShouldRestrictCanAccessDataForOriginToUIThread()) { |
| // Ensure this is only called on the UI thread, which is the only thread |
| // with sufficient information to do the full set of checks. |
| CHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); |
| } else { |
| // For legacy cases, this may be called on multiple threads. |
| DCHECK(IsRunningOnExpectedThread()); |
| } |
| |
| base::AutoLock lock(lock_); |
| |
| SecurityState* security_state = GetSecurityState(child_id); |
| BrowserOrResourceContext browser_or_resource_context; |
| if (security_state) |
| browser_or_resource_context = security_state->GetBrowserOrResourceContext(); |
| |
| ProcessLock expected_process_lock; |
| std::string failure_reason; |
| |
| if (!security_state) { |
| failure_reason = "no_security_state"; |
| } else if (!browser_or_resource_context) { |
| failure_reason = "no_browser_or_resource_context"; |
| } else { |
| ProcessLock actual_process_lock = security_state->process_lock(); |
| |
| // Deny access if the process is unlocked. An unlocked process means that |
| // the process has not been associated with a SiteInstance yet and therefore |
| // this request is likely invalid. |
| if (actual_process_lock.is_invalid()) { |
| failure_reason = "process_lock_is_invalid"; |
| } else { |
| // Loop over all BrowsingInstanceIDs in the SecurityState, and return true |
| // if any of them would return true, otherwise return false. This allows |
| // the checks to be slightly stricter in cases where all BrowsingInstances |
| // agree (e.g., whether an origin is considered isolated and thus |
| // inaccessible from a site-locked process). When the BrowsingInstances |
| // do not agree, the check might be slightly weaker (as the least common |
| // denominator), but the differences must never violate the ProcessLock. |
| if (security_state->browsing_instance_info().empty()) { |
| // If no BrowsingInstances are found, then the some of the state we need |
| // to perform an accurate check is unexpectedly missing, because there |
| // should always be a BrowsingInstance for such requests, even from |
| // workers. Thus, we should usually kill the process in this case, so |
| // that a compromised renderer can't bypass checks by sending IPCs when |
| // no BrowsingInstances are left. |
| // |
| // However, if the requested `url` is compatible with the current |
| // ProcessLock, then there is no need to kill the process because the |
| // checks would have passed anyway. To reduce the number of crashes |
| // while we debug why no BrowsingInstances were found (in |
| // https://crbug.com/1148542), we'll allow requests with an acceptable |
| // process lock to proceed. |
| // TODO(1148542): Remove this when known cases of having no |
| // BrowsingInstance IDs are solved. |
| url::Origin origin(url::Origin::Create(url)); |
| bool matches_origin_keyed_process = |
| actual_process_lock.is_origin_keyed_process() && |
| actual_process_lock.lock_url() == origin.GetURL(); |
| bool matches_site_keyed_process = |
| !actual_process_lock.is_origin_keyed_process() && |
| actual_process_lock.lock_url() == |
| SiteInfo::GetSiteForOrigin(origin); |
| // ProcessLocks with is_pdf() = true actually means that the process is |
| // not supposed to access certain resources from the lock's site/origin, |
| // so it's safest here to fall through in that case. See discussion of |
| // https://crbug.com/1271197 below. |
| if (!actual_process_lock.is_pdf()) { |
| // If the ProcessLock isn't locked to a site, we should fall through |
| // since we have no way of knowing if the requested url was expecting |
| // to be in a locked process. |
| if (actual_process_lock.is_locked_to_site()) { |
| if (matches_origin_keyed_process || matches_site_keyed_process) { |
| return true; |
| } else { |
| failure_reason = base::StringPrintf( |
| "No BrowsingInstanceIDs: Lock Mismatch. lock = %s vs. " |
| "requested_url = %s ", |
| actual_process_lock.ToString().c_str(), url.spec().c_str()); |
| } |
| } else { |
| failure_reason = |
| "No BrowsingInstanceIDs: process not locked to site"; |
| } |
| } else { |
| failure_reason = "No BrowsingInstanceIDs: process lock is_pdf"; |
| } |
| // This will fall through to the call to |
| // LogCanAccessDataForOriginCrashKeys below, then return false. |
| } |
| for (auto browsing_instance_info_entry : |
| security_state->browsing_instance_info()) { |
| auto& browsing_instance_id = browsing_instance_info_entry.first; |
| auto& default_isolation_state = browsing_instance_info_entry.second; |
| // In the case of multiple BrowsingInstances in the SecurityState, note |
| // that failure reasons will only be reported if none of the |
| // BrowsingInstances allow access. In that event, |failure_reason| |
| // contains the concatenated reasons for each BrowsingInstance, each |
| // prefaced by its id. |
| failure_reason += base::StringPrintf( |
| "[BI=%d]", browsing_instance_id.GetUnsafeValue()); |
| |
| // Use the actual process lock's state to compute `is_guest` and |
| // `is_fenced` for the expected process lock's `isolation_context`. |
| // Guest status and fenced status doesn't currently influence the |
| // outcome of this access check, and even if it did, `url` wouldn't be |
| // sufficient to tell whether the request belongs solely to a guest (or |
| // non-guest) or fenced process. Note that a guest isn't allowed to |
| // access data outside of its own StoragePartition, but this is enforced |
| // by other means (e.g., resource access APIs can't name an alternate |
| // StoragePartition). |
| IsolationContext isolation_context( |
| browsing_instance_id, browser_or_resource_context, |
| actual_process_lock.is_guest(), actual_process_lock.is_fenced(), |
| default_isolation_state); |
| |
| // NOTE: If we're on the IO thread, the call to |
| // ProcessLock::Create() below will return a ProcessLock with |
| // an (internally) identical site_url, one that does not use effective |
| // URLs. That's ok in this instance since we only ever look at the lock |
| // url. |
| // |
| // Since we are dealing with a valid ProcessLock at this point, we know |
| // the lock contains a valid StoragePartitionConfig and COOP/COEP |
| // information because that information must be provided when creating |
| // the locks. |
| // |
| // At this point, any origin opt-in isolation requests should be |
| // complete, so to avoid the possibility of opting something set |
| // |origin_isolation_request| to kNone below (this happens by default in |
| // UrlInfoInit's ctor). Note: We might need to revisit this if |
| // CanAccessDataForOrigin() needs to be called while a SiteInstance is |
| // being determined for a navigation, i.e. during |
| // GetSiteInstanceForNavigationRequest(). If this happens, we'd need |
| // to plumb UrlInfo::origin_isolation_request value from the ongoing |
| // NavigationRequest into here. Also, we would likely need to attach |
| // the BrowsingInstanceID to UrlInfo once the SiteInstance has been |
| // determined in case the RenderProcess has multiple BrowsingInstances |
| // in it. |
| // TODO(acolwell): Provide a way for callers, that know their request's |
| // require COOP/COEP handling, to pass in their COOP/COEP information |
| // so it can be used here instead of the values in |
| // |actual_process_lock|. |
| // TODO(crbug.com/1271197): The code below is subtly incorrect in cases |
| // where actual_process_lock.is_pdf() is true, since in the case of PDFs |
| // the lock is intended to prevent access to the lock's site/origin, |
| // while still allowing the navigation to commit. |
| expected_process_lock = ProcessLock::Create( |
| isolation_context, |
| UrlInfo(UrlInfoInit(url) |
| .WithStoragePartitionConfig( |
| actual_process_lock.GetStoragePartitionConfig()) |
| .WithWebExposedIsolationInfo( |
| actual_process_lock.GetWebExposedIsolationInfo()) |
| .WithIsPdf(actual_process_lock.is_pdf()) |
| .WithSandbox(actual_process_lock.is_sandboxed()) |
| .WithUniqueSandboxId( |
| actual_process_lock.unique_sandbox_id()))); |
| |
| if (actual_process_lock.is_locked_to_site()) { |
| // Jail-style enforcement - a process with a lock can only access |
| // data from origins that require exactly the same lock. |
| if (actual_process_lock == expected_process_lock) |
| return true; |
| |
| // TODO(acolwell, nasko): https://crbug.com/1029092: Ensure the |
| // precursor of opaque origins matches the renderer's origin lock. |
| if (url_is_precursor_of_opaque_origin) { |
| const GURL& lock_url = actual_process_lock.lock_url(); |
| // SitePerProcessBrowserTest |
| // .TwoBlobURLsWithNullOriginDontShareProcess. |
| if (lock_url.SchemeIsBlob() && |
| base::StartsWith(lock_url.path_piece(), "null/")) { |
| return true; |
| } |
| |
| // DeclarativeApiTest.PersistRules. |
| if (actual_process_lock.matches_scheme(url::kDataScheme)) |
| return true; |
| } |
| |
| // Make an exception to allow most visited tiles to commit in |
| // third-party NTP processes. |
| // TODO(crbug.com/566091): This exception should be removed once |
| // these tiles can be loaded in OOPIFs on the NTP. |
| if (AllowProcessLockMismatchForNTP(expected_process_lock, |
| actual_process_lock)) { |
| return true; |
| } |
| |
| // TODO(wjmaclean): We should update the ProcessLock comparison API |
| // to return a reason why two locks differ. |
| if (actual_process_lock.lock_url() != |
| expected_process_lock.lock_url()) { |
| failure_reason += "lock_mismatch:url "; |
| // If the actual lock is same-site to the expected lock, then this |
| // is an isolated origins mismatch; in that case we add text to |
| // |failure_reason| to make this case easy to search for. |
| // Note: We don't compare ports, since the mismatch might be between |
| // isolated and non-isolated. |
| url::Origin actual_origin = |
| url::Origin::Create(actual_process_lock.lock_url()); |
| url::Origin expected_origin = |
| url::Origin::Create(expected_process_lock.lock_url()); |
| if (actual_process_lock.lock_url() == |
| SiteInfo::GetSiteForOrigin(expected_origin) || |
| expected_process_lock.lock_url() == |
| SiteInfo::GetSiteForOrigin(actual_origin)) { |
| failure_reason += "[origin vs site mismatch] "; |
| } |
| } else { |
| // TODO(wjmaclean,alexmos): Apparently this might not be true |
| // anymore, since is_pdf() and web_exposed_isolation_info() have |
| // been added to the ProcessLock. We need to update the code here to |
| // differentiate these cases, as well as adding documentation (or |
| // some other mechanism) to prevent these getting out of sync in |
| // future. |
| failure_reason += "lock_mismatch:requires_origin_keyed_process "; |
| } |
| } else { |
| // Citadel-style enforcement - an unlocked process should not be |
| // able to access data from origins that require a lock. |
| |
| // Allow the corresponding base::Feature to turn off enforcement. |
| if (!base::FeatureList::IsEnabled(kSiteIsolationCitadelEnforcement)) { |
| return true; |
| } |
| |
| // Skip these checks on the IO thread, since we can't use |
| // RenderProcessHost or ShouldLockProcessToSite() there. |
| // |
| // TODO(crbug.com/764958): Remove this once this is reachable only on |
| // the UI thread. |
| if (!ShouldRestrictCanAccessDataForOriginToUIThread() && |
| BrowserThread::CurrentlyOn(BrowserThread::IO)) { |
| return true; |
| } |
| |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| // TODO(lukasza): Consider making the checks below IO-thread-friendly, |
| // by storing |is_unused| inside SecurityState. |
| RenderProcessHost* process = RenderProcessHostImpl::FromID(child_id); |
| if (process) { // |process| can be null in unittests |
| // Unlocked process can be legitimately used when navigating from an |
| // unused process (about:blank, NTP on Android) to an isolated |
| // origin. See also https://crbug.com/945399. Returning |true| |
| // below will allow such navigations to succeed (i.e. pass |
| // CanCommitOriginAndUrl checks). We don't expect unused processes |
| // to be used outside of navigations (e.g. when checking |
| // CanAccessDataForOrigin for localStorage, etc.). |
| if (process->IsUnused()) |
| return true; |
| } |
| |
| // See the ProcessLock::Create() call above regarding why we pass |
| // kNone for |origin_isolation_request| below. |
| SiteInfo site_info = SiteInfo::Create( |
| isolation_context, |
| UrlInfo(UrlInfoInit(url).WithWebExposedIsolationInfo( |
| actual_process_lock.GetWebExposedIsolationInfo()))); |
| |
| // A process that's not locked to any site can only access data from |
| // origins that do not require a locked process. |
| if (!site_info.ShouldLockProcessToSite(isolation_context)) |
| return true; |
| |
| failure_reason += " citadel_enforcement "; |
| if (url_is_precursor_of_opaque_origin) { |
| failure_reason += "for_precursor "; |
| } |
| } |
| } |
| } |
| } |
| |
| // Record the duration of KeepAlive requests to include in the crash keys. |
| std::string keep_alive_durations; |
| std::string shutdown_delay_ref_count; |
| std::string process_rfh_count; |
| if (BrowserThread::CurrentlyOn(BrowserThread::UI)) { |
| if (auto* process = RenderProcessHostImpl::FromID(child_id)) { |
| keep_alive_durations = process->GetKeepAliveDurations(); |
| shutdown_delay_ref_count = |
| base::NumberToString(process->GetShutdownDelayRefCount()); |
| process_rfh_count = |
| base::NumberToString(process->GetRenderFrameHostCount()); |
| } |
| } else { |
| keep_alive_durations = "no durations available: on IO thread."; |
| } |
| |
| // Returning false here will result in a renderer kill. Set some crash |
| // keys that will help understand the circumstances of that kill. |
| LogCanAccessDataForOriginCrashKeys( |
| expected_process_lock.ToString(), |
| GetKilledProcessOriginLock(security_state), |
| url.DeprecatedGetOriginAsURL().spec(), failure_reason, |
| keep_alive_durations, shutdown_delay_ref_count, process_rfh_count); |
| return false; |
| } |
| |
| void ChildProcessSecurityPolicyImpl::IncludeIsolationContext( |
| int child_id, |
| const IsolationContext& isolation_context) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::AutoLock lock(lock_); |
| auto* state = GetSecurityState(child_id); |
| DCHECK(state); |
| state->AddBrowsingInstanceInfo(isolation_context); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::LockProcess( |
| const IsolationContext& context, |
| int child_id, |
| bool is_process_used, |
| const ProcessLock& process_lock) { |
| // LockProcess should only be called on the UI thread (OTOH, it is okay to |
| // call GetProcessLock from any thread). |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| base::AutoLock lock(lock_); |
| auto state = security_state_.find(child_id); |
| DCHECK(state != security_state_.end()); |
| state->second->SetProcessLock(process_lock, context, is_process_used); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::LockProcessForTesting( |
| const IsolationContext& isolation_context, |
| int child_id, |
| const GURL& url) { |
| SiteInfo site_info = SiteInfo::CreateForTesting(isolation_context, url); |
| LockProcess(isolation_context, child_id, /* is_process_used=*/false, |
| ProcessLock::FromSiteInfo(site_info)); |
| } |
| |
| ProcessLock ChildProcessSecurityPolicyImpl::GetProcessLock(int child_id) { |
| base::AutoLock lock(lock_); |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return ProcessLock(); |
| return state->second->process_lock(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::GrantPermissionsForFileSystem( |
| int child_id, |
| const std::string& filesystem_id, |
| int permission) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return; |
| state->second->GrantPermissionsForFileSystem(filesystem_id, permission); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::HasPermissionsForFileSystem( |
| int child_id, |
| const std::string& filesystem_id, |
| int permission) { |
| base::AutoLock lock(lock_); |
| |
| auto* state = GetSecurityState(child_id); |
| if (!state) |
| return false; |
| return state->HasPermissionsForFileSystem(filesystem_id, permission); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RegisterFileSystemPermissionPolicy( |
| storage::FileSystemType type, |
| int policy) { |
| base::AutoLock lock(lock_); |
| file_system_policy_map_[type] = policy; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanSendMidiMessage(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) { |
| return false; |
| } |
| |
| return state->second->CanSendMidi(); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::CanSendMidiSysExMessage(int child_id) { |
| base::AutoLock lock(lock_); |
| |
| auto state = security_state_.find(child_id); |
| if (state == security_state_.end()) |
| return false; |
| |
| return state->second->CanSendMidiSysEx(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins( |
| const std::vector<url::Origin>& origins_to_add, |
| IsolatedOriginSource source, |
| BrowserContext* browser_context) { |
| std::vector<IsolatedOriginPattern> patterns; |
| patterns.reserve(origins_to_add.size()); |
| base::ranges::transform( |
| origins_to_add, std::back_inserter(patterns), |
| [](const url::Origin& o) { return IsolatedOriginPattern(o); }); |
| AddFutureIsolatedOrigins(patterns, source, browser_context); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins( |
| base::StringPiece origins_to_add, |
| IsolatedOriginSource source, |
| BrowserContext* browser_context) { |
| std::vector<IsolatedOriginPattern> patterns = |
| ParseIsolatedOrigins(origins_to_add); |
| AddFutureIsolatedOrigins(patterns, source, browser_context); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddFutureIsolatedOrigins( |
| const std::vector<IsolatedOriginPattern>& patterns, |
| IsolatedOriginSource source, |
| BrowserContext* browser_context) { |
| // This can only be called from the UI thread, as it reads state that's only |
| // available (and is only safe to be retrieved) on the UI thread, such as |
| // BrowsingInstance IDs. |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| |
| for (const IsolatedOriginPattern& pattern : patterns) { |
| if (!pattern.is_valid()) { |
| LOG(ERROR) << "Invalid isolated origin: " << pattern.pattern(); |
| continue; |
| } |
| |
| url::Origin origin_to_add = pattern.origin(); |
| |
| // Isolated origins added here should apply only to future |
| // BrowsingInstances and processes. Determine the first BrowsingInstance |
| // ID to which they should apply. |
| BrowsingInstanceId browsing_instance_id = |
| SiteInstanceImpl::NextBrowsingInstanceId(); |
| |
| AddIsolatedOriginInternal(browser_context, origin_to_add, |
| true /* applies_to_future_browsing_instances */, |
| browsing_instance_id, |
| pattern.isolate_all_subdomains(), source); |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddIsolatedOriginInternal( |
| BrowserContext* browser_context, |
| const url::Origin& origin_to_add, |
| bool applies_to_future_browsing_instances, |
| BrowsingInstanceId browsing_instance_id, |
| bool isolate_all_subdomains, |
| IsolatedOriginSource source) { |
| // GetSiteForOrigin() is used to look up the site URL of |origin| to speed |
| // up the isolated origin lookup. This only performs a straightforward |
| // translation of an origin to eTLD+1; it does *not* take into account |
| // effective URLs, isolated origins, and other logic that's not needed |
| // here, but *is* typically needed for making process model decisions. Be |
| // very careful about using GetSiteForOrigin() elsewhere, and consider |
| // whether you should be using SiteInfo::Create() instead. |
| GURL key(SiteInfo::GetSiteForOrigin(origin_to_add)); |
| |
| // Check if the origin to be added already exists, in which case it may not |
| // need to be added again. |
| bool should_add = true; |
| for (const auto& entry : isolated_origins_[key]) { |
| // TODO(alexmos): The exact origin comparison here allows redundant entries |
| // with certain uses of `isolate_all_subdomains`. See |
| // https://crbug.com/1184580. |
| if (entry.origin() != origin_to_add) |
| continue; |
| // If the added origin already exists for the same BrowserContext and |
| // covers the same BrowsingInstances, don't re-add it. |
| if (entry.browser_context() == browser_context) { |
| if (entry.applies_to_future_browsing_instances() && |
| entry.browsing_instance_id() <= browsing_instance_id) { |
| // If the existing entry applies to future BrowsingInstances, and it |
| // has a lower/same BrowsingInstance ID, don't re-add the origin. Note |
| // that if the new isolated origin is also requested to apply to future |
| // BrowsingInstances, the threshold ID must necessarily be greater than |
| // the old ID, since NextBrowsingInstanceId() returns monotonically |
| // increasing IDs. |
| if (applies_to_future_browsing_instances) |
| DCHECK_LE(entry.browsing_instance_id(), browsing_instance_id); |
| should_add = false; |
| break; |
| } else if (!entry.applies_to_future_browsing_instances() && |
| entry.browsing_instance_id() == browsing_instance_id) { |
| // Otherwise, don't re-add the origin if the existing entry is for the |
| // same BrowsingInstance ID. Note that if an origin had been added for |
| // a specific BrowsingInstance, we can't later receive a request to |
| // isolate that origin within future BrowsingInstances that start at |
| // the same (or lower) BrowsingInstance. Requests to isolate future |
| // BrowsingInstances should always reference |
| // SiteInstanceImpl::NextBrowsingInstanceId(), which always refers to |
| // an ID that's greater than any existing BrowsingInstance ID. |
| DCHECK(!applies_to_future_browsing_instances); |
| |
| should_add = false; |
| break; |
| } |
| } |
| |
| // Otherwise, allow the origin to be added again for a different profile |
| // (or globally for all profiles), possibly with a different |
| // BrowsingInstance ID cutoff. Note that a particular origin might have |
| // multiple entries, each one for a different profile, so we must loop |
| // over all such existing entries before concluding that |origin| really |
| // needs to be added. |
| } |
| |
| if (should_add) { |
| ResourceContext* resource_context = |
| browser_context ? browser_context->GetResourceContext() : nullptr; |
| IsolatedOriginEntry entry(std::move(origin_to_add), |
| applies_to_future_browsing_instances, |
| browsing_instance_id, browser_context, |
| resource_context, isolate_all_subdomains, source); |
| isolated_origins_[key].emplace_back(std::move(entry)); |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RemoveStateForBrowserContext( |
| const BrowserContext& browser_context) { |
| { |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| |
| for (auto& iter : isolated_origins_) { |
| base::EraseIf(iter.second, |
| [&browser_context](const IsolatedOriginEntry& entry) { |
| // Remove if BrowserContext matches. |
| return (entry.browser_context() == &browser_context); |
| }); |
| } |
| |
| // Also remove map entries for site URLs which no longer have any |
| // IsolatedOriginEntries remaining. |
| base::EraseIf(isolated_origins_, |
| [](const auto& pair) { return pair.second.empty(); }); |
| } |
| |
| { |
| base::AutoLock lock(lock_); |
| for (auto& pair : security_state_) |
| pair.second->ClearBrowserContextIfMatches(&browser_context); |
| |
| for (auto& pair : pending_remove_state_) |
| pair.second->ClearBrowserContextIfMatches(&browser_context); |
| } |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsIsolatedOrigin( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| bool origin_requests_isolation) { |
| url::Origin unused_result; |
| return GetMatchingProcessIsolatedOrigin( |
| isolation_context, origin, origin_requests_isolation, &unused_result); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsGloballyIsolatedOriginForTesting( |
| const url::Origin& origin) { |
| BrowserOrResourceContext no_browser_context; |
| BrowsingInstanceId null_browsing_instance_id; |
| IsolationContext isolation_context( |
| null_browsing_instance_id, no_browser_context, /*is_guest=*/false, |
| /*is_fenced=*/false, |
| OriginAgentClusterIsolationState::CreateNonIsolated()); |
| return IsIsolatedOrigin(isolation_context, origin, false); |
| } |
| |
| std::vector<url::Origin> ChildProcessSecurityPolicyImpl::GetIsolatedOrigins( |
| absl::optional<IsolatedOriginSource> source, |
| BrowserContext* browser_context) { |
| std::vector<url::Origin> origins; |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| for (const auto& iter : isolated_origins_) { |
| for (const auto& isolated_origin_entry : iter.second) { |
| if (source && source.value() != isolated_origin_entry.source()) |
| continue; |
| |
| // If browser_context is specified, ensure that the entry matches it. If |
| // the browser_context is not specified, only consider entries that are |
| // not associated with a profile (i.e., which apply globally to the |
| // entire browser). |
| bool matches_profile = |
| browser_context ? isolated_origin_entry.MatchesProfile( |
| BrowserOrResourceContext(browser_context)) |
| : isolated_origin_entry.AppliesToAllBrowserContexts(); |
| if (!matches_profile) |
| continue; |
| |
| // Do not include origins that only apply to specific BrowsingInstances. |
| if (!isolated_origin_entry.applies_to_future_browsing_instances()) |
| continue; |
| |
| origins.push_back(isolated_origin_entry.origin()); |
| } |
| } |
| return origins; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::IsIsolatedSiteFromSource( |
| const url::Origin& origin, |
| IsolatedOriginSource source) { |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| GURL site_url = SiteInfo::GetSiteForOrigin(origin); |
| auto it = isolated_origins_.find(site_url); |
| if (it == isolated_origins_.end()) |
| return false; |
| url::Origin site_origin = url::Origin::Create(site_url); |
| for (const auto& entry : it->second) { |
| if (entry.source() == source && entry.origin() == site_origin) |
| return true; |
| } |
| return false; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::GetMatchingProcessIsolatedOrigin( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| bool requests_origin_keyed_process, |
| url::Origin* result) { |
| // GetSiteForOrigin() is used to look up the site URL of |origin| to speed |
| // up the isolated origin lookup. This only performs a straightforward |
| // translation of an origin to eTLD+1; it does *not* take into account |
| // effective URLs, isolated origins, and other logic that's not needed |
| // here, but *is* typically needed for making process model decisions. Be |
| // very careful about using GetSiteForOrigin() elsewhere, and consider |
| // whether you should be using GetSiteForURL() instead. |
| return GetMatchingProcessIsolatedOrigin( |
| isolation_context, origin, requests_origin_keyed_process, |
| SiteInfo::GetSiteForOrigin(origin), result); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::GetMatchingProcessIsolatedOrigin( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| bool requests_origin_keyed_process, |
| const GURL& site_url, |
| url::Origin* result) { |
| DCHECK(IsRunningOnExpectedThread()); |
| |
| *result = url::Origin(); |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| |
| // If |isolation_context| does not specify a BrowsingInstance ID, then assume |
| // that we want to retrieve the latest applicable information; i.e., return |
| // the latest matching isolated origins that would apply to future |
| // BrowsingInstances. Using NextBrowsingInstanceId() will match all |
| // available IsolatedOriginEntries. |
| BrowsingInstanceId browsing_instance_id( |
| isolation_context.browsing_instance_id()); |
| |
| if (browsing_instance_id.is_null()) { |
| browsing_instance_id = SiteInstanceImpl::NextBrowsingInstanceId(); |
| } else { |
| // Check the opt-in isolation status of |origin| in |isolation_context|. |
| // Note that while IsolatedOrigins considers any sub-origin of an isolated |
| // origin as also being isolated, with opt-in we will always either return |
| // false, or true with result set to |origin|. We give priority to origins |
| // requesting opt-in isolation over command-line isolation, but don't check |
| // for opt-in if we didn't get a valid BrowsingInstance id. |
| // Note: This should only return a full origin if we are doing |
| // process-isolated Origin-keyed Agent Clusters, which will only be the case |
| // when site-isolation is enabled. Otherwise we put the origin into its |
| // corresponding site, even if Origin-keyed Agent Clusters will be enabled |
| // on the renderer side. |
| // TODO(wjmaclean,alexmos,acolwell): We should revisit this when we have |
| // SiteInstanceGroups, since at that point we can again return an origin |
| // here (and thus create a new SiteInstance) even when |
| // IsProcessIsolationForOriginAgentClusterEnabled() returns false; in that |
| // case a SiteInstanceGroup will allow a logical group of SiteInstances that |
| // live same-process. |
| if (SiteIsolationPolicy::IsProcessIsolationForOriginAgentClusterEnabled()) { |
| OriginAgentClusterIsolationState oac_isolation_state_request = |
| requests_origin_keyed_process |
| ? OriginAgentClusterIsolationState::CreateForOriginAgentCluster( |
| true /* requires_origin_keyed_process */) |
| : OriginAgentClusterIsolationState::CreateNonIsolated(); |
| OriginAgentClusterIsolationState oac_isolation_state_result = |
| DetermineOriginAgentClusterIsolation(isolation_context, origin, |
| oac_isolation_state_request); |
| if (oac_isolation_state_result.requires_origin_keyed_process()) { |
| *result = origin; |
| return true; |
| } |
| } |
| } |
| |
| // Look up the list of origins corresponding to |origin|'s site. |
| auto it = isolated_origins_.find(site_url); |
| |
| // Subtle corner case: if the site's host ends with a dot, do the lookup |
| // without it. A trailing dot shouldn't be able to bypass isolated origins: |
| // if "https://foo.com" is an isolated origin, "https://foo.com." should |
| // match it. |
| if (it == isolated_origins_.end() && site_url.has_host() && |
| site_url.host_piece().back() == '.') { |
| GURL::Replacements replacements; |
| base::StringPiece host(site_url.host_piece()); |
| host.remove_suffix(1); |
| replacements.SetHostStr(host); |
| it = isolated_origins_.find(site_url.ReplaceComponents(replacements)); |
| } |
| |
| // Looks for all isolated origins that were already isolated at the time |
| // |isolation_context| was created. If multiple isolated origins are |
| // registered with a common domain suffix, return the most specific one. For |
| // example, if foo.isolated.com and isolated.com are both isolated origins, |
| // bar.foo.isolated.com should return foo.isolated.com. |
| bool found = false; |
| if (it != isolated_origins_.end()) { |
| for (const auto& isolated_origin_entry : it->second) { |
| // If this isolated origin applies only to a specific profile, don't |
| // use it for a different profile. |
| if (!isolated_origin_entry.MatchesProfile( |
| isolation_context.browser_or_resource_context())) |
| continue; |
| |
| if (isolated_origin_entry.MatchesBrowsingInstance(browsing_instance_id) && |
| IsolatedOriginUtil::DoesOriginMatchIsolatedOrigin( |
| origin, isolated_origin_entry.origin())) { |
| // If a match has been found that requires all subdomains to be isolated |
| // then return immediately. |origin| is returned to ensure proper |
| // process isolation, e.g. https://a.b.c.isolated.com matches an |
| // IsolatedOriginEntry constructed from http://[*.]isolated.com, so |
| // https://a.b.c.isolated.com must be returned. |
| if (isolated_origin_entry.isolate_all_subdomains()) { |
| *result = origin; |
| uint16_t default_port = url::DefaultPortForScheme( |
| origin.scheme().data(), origin.scheme().length()); |
| |
| if (origin.port() != default_port) { |
| *result = url::Origin::Create(GURL(origin.scheme() + |
| url::kStandardSchemeSeparator + |
| origin.host())); |
| } |
| |
| return true; |
| } |
| |
| if (!found || result->host().length() < |
| isolated_origin_entry.origin().host().length()) { |
| *result = isolated_origin_entry.origin(); |
| found = true; |
| } |
| } |
| } |
| } |
| |
| return found; |
| } |
| |
| OriginAgentClusterIsolationState |
| ChildProcessSecurityPolicyImpl::DetermineOriginAgentClusterIsolation( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| const OriginAgentClusterIsolationState& requested_isolation_state) { |
| if (!IsolatedOriginUtil::IsValidOriginForOptInIsolation(origin)) |
| return OriginAgentClusterIsolationState::CreateNonIsolated(); |
| |
| // See if the same origin exists in the BrowsingInstance already, and if so |
| // return its isolation status. |
| // There are two cases we're worried about here: (i) we've previously seen the |
| // origin and isolated it, in which case we should continue to isolate it, and |
| // (ii) we've previously seen the origin and *not* isolated it, in which case |
| // we should continue to not isolate it. |
| BrowsingInstanceId browsing_instance_id( |
| isolation_context.browsing_instance_id()); |
| |
| if (!browsing_instance_id.is_null()) { |
| base::AutoLock origins_isolation_opt_in_lock( |
| origins_isolation_opt_in_lock_); |
| |
| // Look for |origin| in the isolation status list. |
| OriginAgentClusterIsolationState* oac_isolation_state = |
| LookupOriginIsolationState(browsing_instance_id, origin); |
| |
| if (oac_isolation_state) |
| return *oac_isolation_state; |
| } |
| |
| // If we get to this point, then |origin| is neither opted-in nor opted-out. |
| // At this point we allow opting in if it's requested. This is true for |
| // either logical OriginAgentCluster, or OriginAgentCluster with an |
| // origin-keyed process. |
| return requested_isolation_state; |
| } |
| |
| bool ChildProcessSecurityPolicyImpl:: |
| HasOriginEverRequestedOriginAgentClusterValue( |
| BrowserContext* browser_context, |
| const url::Origin& origin) { |
| base::AutoLock origins_isolation_opt_in_lock(origins_isolation_opt_in_lock_); |
| return base::Contains(origin_isolation_opt_ins_and_outs_, browser_context) && |
| base::Contains(origin_isolation_opt_ins_and_outs_[browser_context], |
| origin); |
| } |
| |
| OriginAgentClusterIsolationState* |
| ChildProcessSecurityPolicyImpl::LookupOriginIsolationState( |
| const BrowsingInstanceId& browsing_instance_id, |
| const url::Origin& origin) { |
| auto it_isolation_by_browsing_instance = |
| origin_isolation_by_browsing_instance_.find(browsing_instance_id); |
| if (it_isolation_by_browsing_instance == |
| origin_isolation_by_browsing_instance_.end()) { |
| return nullptr; |
| } |
| auto& origin_list = it_isolation_by_browsing_instance->second; |
| auto it_origin_list = base::ranges::find( |
| origin_list, origin, &OriginAgentClusterOptInEntry::origin); |
| if (it_origin_list != origin_list.end()) |
| return &(it_origin_list->oac_isolation_state); |
| return nullptr; |
| } |
| |
| OriginAgentClusterIsolationState* |
| ChildProcessSecurityPolicyImpl::LookupOriginIsolationStateForTesting( |
| const BrowsingInstanceId& browsing_instance_id, |
| const url::Origin& origin) { |
| base::AutoLock lock(origins_isolation_opt_in_lock_); |
| return LookupOriginIsolationState(browsing_instance_id, origin); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddDefaultIsolatedOriginIfNeeded( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| bool is_global_walk_or_frame_removal) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| if (!IsolatedOriginUtil::IsValidOriginForOptInIsolation(origin)) |
| return; |
| |
| BrowsingInstanceId browsing_instance_id( |
| isolation_context.browsing_instance_id()); |
| // All callers to this function live on the UI thread, so the IsolationContext |
| // should contain a BrowserContext*. |
| BrowserContext* browser_context = |
| isolation_context.browser_or_resource_context().ToBrowserContext(); |
| DCHECK(browser_context); |
| CHECK(!browsing_instance_id.is_null()); |
| |
| base::AutoLock origins_isolation_opt_in_lock(origins_isolation_opt_in_lock_); |
| |
| // Commits of origins that have ever sent the OriginAgentCluster header in |
| // this BrowserContext are tracked in every BrowsingInstance in this |
| // BrowserContext, to avoid having to do multiple global walks. If the origin |
| // isn't in the list of such origins (i.e., the common case), return early to |
| // avoid unnecessary work, since this is called on every commit. Skip this |
| // during global walks and frame removals, since we do want to track the |
| // origin's non-isolated status in those cases. |
| if (!is_global_walk_or_frame_removal && |
| !(base::Contains(origin_isolation_opt_ins_and_outs_, browser_context) && |
| base::Contains(origin_isolation_opt_ins_and_outs_[browser_context], |
| origin))) { |
| return; |
| } |
| |
| // If |origin| is already in the opt-in-out list, then we don't want to add it |
| // to the list. Technically this check is unnecessary during global |
| // walks (when the origin won't be in this list yet), but it matters during |
| // frame removal (when we don't want to add an opted-in origin to the |
| // list as non-isolated when its frame is removed). |
| if (LookupOriginIsolationState(browsing_instance_id, origin)) { |
| return; |
| } |
| |
| // Since there was no prior record for this BrowsingInstance, track that this |
| // origin should use the default isolation model in use by the |
| // BrowsingInstance. |
| origin_isolation_by_browsing_instance_[browsing_instance_id].emplace_back( |
| isolation_context.default_isolation_state(), origin); |
| } |
| |
| void ChildProcessSecurityPolicyImpl:: |
| RemoveOptInIsolatedOriginsForBrowsingInstance( |
| const BrowsingInstanceId& browsing_instance_id) { |
| // After a suitable delay, remove this BrowsingInstance's info from any |
| // SecurityStates that are using it. |
| // TODO(wjmaclean): Monitor the CanAccessDataForOrigin crash key in renderer |
| // kills to see if we get post-BrowsingInstance-destruction ProcessLock |
| // mismatches, indicating this cleanup should be further delayed. |
| auto task_closure = [](const BrowsingInstanceId id) { |
| ChildProcessSecurityPolicyImpl* policy = |
| ChildProcessSecurityPolicyImpl::GetInstance(); |
| policy->RemoveOptInIsolatedOriginsForBrowsingInstanceInternal(id); |
| }; |
| if (browsing_instance_cleanup_delay_.is_positive()) { |
| // Do the actual state cleanup after posting a task to the IO thread, to |
| // give a chance for any last unprocessed tasks to be handled. The cleanup |
| // itself locks the data structures and can safely happen from either |
| // thread. |
| GetIOThreadTaskRunner({})->PostDelayedTask( |
| FROM_HERE, base::BindOnce(task_closure, browsing_instance_id), |
| browsing_instance_cleanup_delay_); |
| } else { |
| // Since this is just used in tests, it's ok to do it on either thread. |
| task_closure(browsing_instance_id); |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl:: |
| RemoveOptInIsolatedOriginsForBrowsingInstanceInternal( |
| const BrowsingInstanceId browsing_instance_id) { |
| // If a BrowsingInstance is destructing, we should always have an id for it. |
| CHECK(!browsing_instance_id.is_null()); |
| |
| { |
| // content_unittests don't always report being on the IO thread. |
| DCHECK(IsRunningOnExpectedThread()); |
| base::AutoLock lock(lock_); |
| for (auto& it : security_state_) |
| it.second->ClearBrowsingInstanceId(browsing_instance_id); |
| // Note: if the BrowsingInstanceId set is empty at the end of this function, |
| // we must never remove the ProcessLock in case the associated RenderProcess |
| // is compromised, in which case we wouldn't want to reuse it for another |
| // origin. |
| } |
| |
| { |
| base::AutoLock origins_isolation_opt_in_lock( |
| origins_isolation_opt_in_lock_); |
| origin_isolation_by_browsing_instance_.erase(browsing_instance_id); |
| } |
| |
| { |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| for (auto& iter : isolated_origins_) { |
| base::EraseIf(iter.second, [&browsing_instance_id]( |
| const IsolatedOriginEntry& entry) { |
| // Remove entries that are specific to `browsing_instance_id` and |
| // do not apply to future BrowsingInstances. |
| return (entry.browsing_instance_id() == browsing_instance_id && |
| !entry.applies_to_future_browsing_instances()); |
| }); |
| } |
| } |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddCoopIsolatedOriginForBrowsingInstance( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| IsolatedOriginSource source) { |
| // We ought to have validated the origin prior to getting here. If the |
| // origin isn't valid at this point, something has gone wrong. |
| CHECK(IsolatedOriginUtil::IsValidIsolatedOrigin(origin)) |
| << "Trying to isolate invalid origin: " << origin; |
| |
| // This can only be called from the UI thread, as it reads state that's only |
| // available (and is only safe to be retrieved) on the UI thread, such as |
| // BrowsingInstance IDs. |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| |
| BrowsingInstanceId browsing_instance_id( |
| isolation_context.browsing_instance_id()); |
| // This function should only be called when a BrowsingInstance is registering |
| // a new SiteInstance, so |browsing_instance_id| should always be defined. |
| CHECK(!browsing_instance_id.is_null()); |
| |
| // For site-keyed isolation, add `origin` to the isolated_origins_ map (which |
| // supports subdomain matching). |
| // Ensure that `origin` is a site (scheme + eTLD+1) rather than any origin. |
| auto site_origin = url::Origin::Create(SiteInfo::GetSiteForOrigin(origin)); |
| CHECK_EQ(origin, site_origin); |
| |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| |
| // Explicitly set `applies_to_future_browsing_instances` to false to only |
| // isolate `origin` within the provided BrowsingInstance, but not future |
| // ones. Note that it's possible for `origin` to also become isolated for |
| // future BrowsingInstances if AddFutureIsolatedOrigins() is called for it |
| // later. |
| AddIsolatedOriginInternal( |
| isolation_context.browser_or_resource_context().ToBrowserContext(), |
| origin, false /* applies_to_future_browsing_instances */, |
| isolation_context.browsing_instance_id(), |
| false /* isolate_all_subdomains */, source); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::AddOriginIsolationStateForBrowsingInstance( |
| const IsolationContext& isolation_context, |
| const url::Origin& origin, |
| bool is_origin_agent_cluster, |
| bool requires_origin_keyed_process) { |
| // This can only be called from the UI thread, as it reads state that's only |
| // available (and is only safe to be retrieved) on the UI thread, such as |
| // BrowsingInstance IDs. |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| DCHECK( |
| is_origin_agent_cluster || |
| SiteIsolationPolicy::AreOriginAgentClustersEnabledByDefault( |
| isolation_context.browser_or_resource_context().ToBrowserContext())); |
| |
| // We ought to have validated the origin prior to getting here. If the |
| // origin isn't valid at this point, something has gone wrong. |
| CHECK((is_origin_agent_cluster && |
| IsolatedOriginUtil::IsValidOriginForOptInIsolation(origin)) || |
| // The second part of this check is specific to OAC-by-default, and is |
| // required to allow explicit opt-outs for HTTP schemed origins. See |
| // OriginAgentClusterInsecureEnabledBrowserTest.DocumentDomain_Disabled. |
| IsolatedOriginUtil::IsValidOriginForOptOutIsolation(origin)) |
| << "Trying to isolate invalid origin: " << origin; |
| |
| BrowsingInstanceId browsing_instance_id( |
| isolation_context.browsing_instance_id()); |
| // This function should only be called when a BrowsingInstance is registering |
| // a new SiteInstance, so |browsing_instance_id| should always be defined. |
| CHECK(!browsing_instance_id.is_null()); |
| |
| // For origin-keyed isolation, use the origin_isolation_by_browsing_instance_ |
| // map. |
| base::AutoLock origins_isolation_opt_in_lock(origins_isolation_opt_in_lock_); |
| auto it = origin_isolation_by_browsing_instance_.find(browsing_instance_id); |
| if (it == origin_isolation_by_browsing_instance_.end()) { |
| std::tie(it, std::ignore) = origin_isolation_by_browsing_instance_.emplace( |
| browsing_instance_id, std::vector<OriginAgentClusterOptInEntry>()); |
| } |
| |
| // We only support adding new entries, not modifying existing ones. If at |
| // some point in the future we allow isolation status to change during the |
| // lifetime of a BrowsingInstance, then this will need to be updated. |
| if (!base::Contains(it->second, origin, |
| &OriginAgentClusterOptInEntry::origin)) { |
| it->second.emplace_back( |
| is_origin_agent_cluster |
| ? OriginAgentClusterIsolationState::CreateForOriginAgentCluster( |
| requires_origin_keyed_process) |
| : OriginAgentClusterIsolationState::CreateNonIsolated(), |
| origin); |
| } |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::UpdateOriginIsolationOptInListIfNecessary( |
| BrowserContext* browser_context, |
| const url::Origin& origin) { |
| if (!IsolatedOriginUtil::IsValidOriginForOptInIsolation(origin)) |
| return false; |
| |
| base::AutoLock origins_isolation_opt_in_lock(origins_isolation_opt_in_lock_); |
| |
| if (base::Contains(origin_isolation_opt_ins_and_outs_, browser_context) && |
| base::Contains(origin_isolation_opt_ins_and_outs_[browser_context], |
| origin)) { |
| return false; |
| } |
| |
| origin_isolation_opt_ins_and_outs_[browser_context].insert(origin); |
| return true; |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RemoveIsolatedOriginForTesting( |
| const url::Origin& origin) { |
| GURL key(SiteInfo::GetSiteForOrigin(origin)); |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| base::EraseIf(isolated_origins_[key], |
| [&origin](const IsolatedOriginEntry& entry) { |
| // Remove if origin matches. |
| return (entry.origin() == origin); |
| }); |
| if (isolated_origins_[key].empty()) |
| isolated_origins_.erase(key); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::ClearIsolatedOriginsForTesting() { |
| base::AutoLock isolated_origins_lock(isolated_origins_lock_); |
| isolated_origins_.clear(); |
| } |
| |
| ChildProcessSecurityPolicyImpl::SecurityState* |
| ChildProcessSecurityPolicyImpl::GetSecurityState(int child_id) { |
| auto itr = security_state_.find(child_id); |
| if (itr != security_state_.end()) |
| return itr->second.get(); |
| |
| auto pending_itr = pending_remove_state_.find(child_id); |
| if (pending_itr == pending_remove_state_.end()) |
| return nullptr; |
| |
| // At this point the SecurityState in the map is being kept alive |
| // by a Handle object or we are waiting for the deletion task to be run on |
| // the IO thread. |
| SecurityState* pending_security_state = pending_itr->second.get(); |
| |
| auto count_itr = process_reference_counts_.find(child_id); |
| if (count_itr != process_reference_counts_.end()) { |
| // There must be a Handle that still holds a reference to this |
| // pending state so it is safe to return. The assumption is that the |
| // owner of this Handle is making a security check. |
| return pending_security_state; |
| } |
| |
| // Since we don't have an entry in |process_reference_counts_| it means |
| // that we are waiting for the deletion task posted to the IO thread to run. |
| // Only allow the state to be accessed by the IO thread in this situation. |
| if (BrowserThread::CurrentlyOn(BrowserThread::IO)) |
| return pending_security_state; |
| |
| return nullptr; |
| } |
| |
| std::vector<IsolatedOriginPattern> |
| ChildProcessSecurityPolicyImpl::ParseIsolatedOrigins( |
| base::StringPiece pattern_list) { |
| std::vector<base::StringPiece> origin_strings = base::SplitStringPiece( |
| pattern_list, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| |
| std::vector<IsolatedOriginPattern> patterns; |
| patterns.reserve(origin_strings.size()); |
| |
| for (const base::StringPiece& origin_string : origin_strings) |
| patterns.emplace_back(origin_string); |
| |
| return patterns; |
| } |
| |
| // static |
| std::string ChildProcessSecurityPolicyImpl::GetKilledProcessOriginLock( |
| const SecurityState* security_state) { |
| if (!security_state) |
| return "(child id not found)"; |
| |
| if (!security_state->GetBrowserOrResourceContext()) |
| return "(empty and null context)"; |
| |
| return security_state->process_lock().ToString(); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::LogKilledProcessOriginLock(int child_id) { |
| base::AutoLock lock(lock_); |
| const auto itr = security_state_.find(child_id); |
| const SecurityState* security_state = |
| itr != security_state_.end() ? itr->second.get() : nullptr; |
| |
| base::debug::SetCrashKeyString(GetKilledProcessOriginLockKey(), |
| GetKilledProcessOriginLock(security_state)); |
| } |
| |
| ChildProcessSecurityPolicyImpl::Handle |
| ChildProcessSecurityPolicyImpl::CreateHandle(int child_id) { |
| return Handle(child_id, /* duplicating_handle */ false); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::AddProcessReference( |
| int child_id, |
| bool duplicating_handle) { |
| base::AutoLock lock(lock_); |
| return AddProcessReferenceLocked(child_id, duplicating_handle); |
| } |
| |
| bool ChildProcessSecurityPolicyImpl::AddProcessReferenceLocked( |
| int child_id, |
| bool duplicating_handle) { |
| if (child_id == ChildProcessHost::kInvalidUniqueID) |
| return false; |
| |
| // Check to see if the SecurityState has been removed from |security_state_| |
| // via a Remove() call. This corresponds to the process being destroyed. |
| if (security_state_.find(child_id) == security_state_.end()) { |
| if (!duplicating_handle) { |
| // Do not allow Handles to be created after the process has been |
| // destroyed, unless they are being duplicated. |
| return false; |
| } |
| |
| // The process has been destroyed but we are allowing an existing Handle |
| // to be duplicated. Verify that the process reference count is available |
| // and indicates another Handle has a reference. |
| auto itr = process_reference_counts_.find(child_id); |
| CHECK(itr != process_reference_counts_.end()); |
| CHECK_GT(itr->second, 0); |
| } |
| |
| ++process_reference_counts_[child_id]; |
| return true; |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RemoveProcessReference(int child_id) { |
| base::AutoLock lock(lock_); |
| RemoveProcessReferenceLocked(child_id); |
| } |
| |
| void ChildProcessSecurityPolicyImpl::RemoveProcessReferenceLocked( |
| int child_id) { |
| auto itr = process_reference_counts_.find(child_id); |
| CHECK(itr != process_reference_counts_.end()); |
| |
| if (itr->second > 1) { |
| itr->second--; |
| return; |
| } |
| |
| DCHECK_EQ(itr->second, 1); |
| process_reference_counts_.erase(itr); |
| |
| // |child_id| could be inside tasks that are on the IO thread task queues. We |
| // need to keep the |pending_remove_state_| entry around until we have |
| // successfully executed a task on the IO thread. This should ensure that any |
| // pending tasks on the IO thread will have completed before we remove the |
| // entry. |
| // TODO(acolwell): Remove this call once all objects on the IO thread have |
| // been converted to use Handles. |
| GetIOThreadTaskRunner({})->PostTask( |
| FROM_HERE, base::BindOnce( |
| [](ChildProcessSecurityPolicyImpl* policy, int child_id) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| base::AutoLock lock(policy->lock_); |
| policy->pending_remove_state_.erase(child_id); |
| }, |
| base::Unretained(this), child_id)); |
| } |
| |
| } // namespace content |