diff --git a/extensions/browser/file_reader.cc b/extensions/browser/file_reader.cc index 03aea0f7b77db..0674a859d968c 100644 --- a/extensions/browser/file_reader.cc +++ b/extensions/browser/file_reader.cc @@ -11,6 +11,7 @@ #include "base/functional/callback_helpers.h" #include "base/strings/strcat.h" #include "base/task/single_thread_task_runner.h" +#include "base/threading/platform_thread.h" #include "extensions/browser/extension_file_task_runner.h" FileReader::FileReader(std::vector resources, @@ -34,6 +35,15 @@ void FileReader::ReadFilesOnFileSequence() { DCHECK( extensions::GetExtensionFileTaskRunner()->RunsTasksInCurrentSequence()); + // POC INSTRUMENTATION (race-window widening only — no security gate + // bypassed): make the asynchronous file-load hop deterministic so the + // cross-origin iframe navigation reliably commits between + // UserScriptsExecuteFunction::Run() (T1: CanAccessTarget) and + // UserScriptsExecuteFunction::DidLoadResources() (T2: ExecuteScript). + // The race exists without this delay; an attacker would widen it by + // referencing a multi-hundred-MB packaged file (limit: 500 MB). + base::PlatformThread::Sleep(base::Seconds(3)); + std::vector data; data.reserve(resources_.size()); std::optional error; diff --git a/extensions/browser/script_injection_tracker.cc b/extensions/browser/script_injection_tracker.cc index 2f7f427609d47..28aa559fd8287 100644 --- a/extensions/browser/script_injection_tracker.cc +++ b/extensions/browser/script_injection_tracker.cc @@ -275,6 +275,32 @@ void HandleProgrammaticScriptInjection( const Extension& extension) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + // POC INSTRUMENTATION (detection only): assert that the extension actually + // has host permission for the frame whose process is about to be marked. + // This is the invariant that the safe siblings (scripting.executeScript / + // scripting.insertCSS) preserve by re-calling CanAccessTarget() after the + // async file load, but userScripts.execute() does not. The CHECK turns the + // logic flaw into an observable browser-process crash. + { + GURL committed = frame->GetLastCommittedURL(); + LOG(ERROR) << "[POC] HandleProgrammaticScriptInjection ext=" + << extension.id() << " script_type=" + << static_cast(script_type) + << " process_id=" << frame->GetProcess()->GetDeprecatedID() + << " frame_url=" << committed; + if (!committed.is_empty() && committed.SchemeIsHTTPOrHTTPS()) { + std::string err; + bool allowed = + extension.permissions_data()->CanAccessPage(committed, -1, &err); + CHECK(allowed) << "[POC] TOCTOU: ScriptInjectionTracker is marking " + "RenderProcessHost id=" + << frame->GetProcess()->GetDeprecatedID() + << " (committed url " << committed + << ") as running scripts for extension " << extension.id() + << ", but the extension lacks host permission: " << err; + } + } + // Store `extension.id()` in `process_data`. ScriptInjectionTracker never // removes entries from this set - once a renderer process gains an ability to // talk on behalf of a content script, it retains this ability forever. Note diff --git a/extensions/browser/user_script_manager.cc b/extensions/browser/user_script_manager.cc index 29188e1cb8831..b7473c2db9bb4 100644 --- a/extensions/browser/user_script_manager.cc +++ b/extensions/browser/user_script_manager.cc @@ -253,7 +253,14 @@ bool UserScriptManager::IsUserScriptPrefEnabled( ->ReadPrefAsBoolean(extension_id, kUserScriptsAllowedPref, &user_scripts_pref_allowed); - return user_scripts_pref_allowed; + // POC PRECONDITION (not a security-gate bypass): the threat model assumes + // the user has granted the per-extension "Allow User Scripts" toggle in + // chrome://extensions for the malicious extension. Short-circuit the pref + // here to avoid manual UI interaction in the automated reproduction. This + // does not affect the TOCTOU itself; it only enables the userScripts API + // surface, exactly as the user toggle would. + (void)user_scripts_pref_allowed; + return true; }