Low chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in Actor
DescriptionMissing authorization in Actor
ComponentActor
Sub-componentGlic
Bug ClassAuthorization Error
Tracker502282293
Fix commitc083e5ef61b0 (chromium/src) +144/-28
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/browser/glic/actor/glic_actor_task_manager.cc
modified
switch
chrome/browser/glic/actor/glic_actor_task_manager.cc
modified
BindRepeating
chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
modified

Files Changed

  • chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
  • chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
  • chrome/browser/glic/actor/glic_actor_task_manager.cc
  • chrome/browser/glic/actor/glic_actor_task_manager.h
From c083e5ef61b0b64e767ad8580327041525d53e04 Mon Sep 17 00:00:00 2001
From: mark a. foltz <mfoltz@chromium.org>
Date: Fri, 04 Sep 2026 11:25:31 -0700
Subject: [PATCH] [glic] Validate task ID matches current task in GlicActorClientSession.

- Adds ValidateTaskIdMatchesCurrent in GlicActorClientSession to ensure
  that glic calls that take a task ID only operate on the session's
  active current_task_id_.
- Adds browser tests in GlicActorActionExecutionFunctionalBrowserTest
  verifying that mismatched task IDs are rejected for PerformActions
  and CancelActions.

Fixed: 502282293
Change-Id: I05bc20a54de9ba85a83d9854ee01f792f6e2f4d9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8333560
Commit-Queue: Mark Foltz <mfoltz@chromium.org>
Reviewed-by: Justin DeWitt <dewittj@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1692616}
---
diff --git a/chrome/browser/glic/actor/glic_actor_task_manager.cc b/chrome/browser/glic/actor/glic_actor_task_manager.cc
index 452e131..710796bb 100644
--- a/chrome/browser/glic/actor/glic_actor_task_manager.cc
+++ b/chrome/browser/glic/actor/glic_actor_task_manager.cc
@@ -462,7 +462,8 @@
   }
 
   actor::TaskId task_id(actions.task_id());
-  if (!actor_keyed_service().GetTask(task_id)) {
+  if (!ValidateTaskIdMatchesCurrent(task_id, "GlicPerformActions") ||
+      !actor_keyed_service().GetTask(task_id)) {
     actor_keyed_service().GetJournal().Log(GURL::EmptyGURL(), task_id,
                                            "Act Failed",
                                            actor::JournalDetailsBuilder()
@@ -511,6 +512,10 @@
 void GlicActorClientSession::CancelActions(int32_t task_id,
                                            CancelActionsCallback callback) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "CancelActions")) {
+    std::move(callback).Run(mojom::CancelActionsResult::kTaskNotFound);
+    return;
+  }
   actor::ActorTask* task = actor_keyed_service().GetTask(actor_task_id);
   if (!task) {
     std::move(callback).Run(mojom::CancelActionsResult::kTaskNotFound);
@@ -529,6 +534,9 @@
     int32_t task_id,
     mojom::ActorTaskStopReason stop_reason) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "StopActorTask")) {
+    return;
+  }
   instance_metrics().OnStopActorTask();
   actor::ActorTask::StoppedReason reason;
   switch (stop_reason) {
@@ -572,6 +580,9 @@
     mojom::ActorTaskPauseReason pause_reason,
     std::optional<int32_t> tab_handle) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "PauseActorTask")) {
+    return;
+  }
   tabs::TabInterface::Handle handle;
   if (tab_handle.has_value()) {
     handle = tabs::TabInterface::Handle(*tab_handle);
@@ -604,6 +615,12 @@
     mojom::TabContextOptionsPtr context_options,
     ResumeActorTaskCallback callback) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "ResumeActorTask")) {
+    std::string error_message = "No such task";
+    std::move(callback).Run(mojom::GetContextResultWithActionResultCode::New(
+        mojom::GetContextResult::NewErrorReason(error_message), std::nullopt));
+    return;
+  }
   instance_metrics().OnResumeActorTask();
   actor::ActorTask* task = actor_keyed_service().GetTask(actor_task_id);
   if (!task || !task->IsUnderUserControl()) {
@@ -769,6 +786,9 @@
     int32_t task_id,
     std::optional<mojom::ActorTaskInterruptReason> interrupt_reason) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "InterruptActorTask")) {
+    return;
+  }
   instance_metrics().InterruptActorTask();
 
   actor::ActorTask* task = actor_keyed_service().GetTask(actor_task_id);
@@ -789,6 +809,9 @@
 
 void GlicActorClientSession::UninterruptActorTask(int32_t task_id) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "UninterruptActorTask")) {
+    return;
+  }
   instance_metrics().UninterruptActorTask();
   actor::ActorTask* task = actor_keyed_service().GetTask(actor_task_id);
   if (!task) {
@@ -811,6 +834,10 @@
     int32_t task_id,
     const std::string& step_progress) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id,
+                                    "UpdateActorTaskStepProgress")) {
+    return;
+  }
   actor::ActorTask* task = actor_keyed_service().GetTask(actor_task_id);
   if (!task) {
     actor_keyed_service().GetJournal().Log(
@@ -830,6 +857,10 @@
     mojom::CreateActorTabOptionsPtr options,
     CreateActorTabCallback callback) {
   auto actor_task_id = actor::TaskId(task_id);
+  if (!ValidateTaskIdMatchesCurrent(actor_task_id, "CreateActorTab")) {
+    std::move(callback).Run(nullptr);
+    return;
+  }
   tabs::TabHandle initiator_tab_handle =
       options->initiator_tab_id.has_value()
           ? tabs::TabHandle(*options->initiator_tab_id)
@@ -936,6 +967,22 @@
   actor_keyed_service().StopTask(task->id(), reason);
 }
 
+bool GlicActorClientSession::ValidateTaskIdMatchesCurrent(
+    actor::TaskId task_id,
+    std::string_view method_name) {
+  if (current_task_id_.is_null() || task_id != current_task_id_) {
+    actor_keyed_service().GetJournal().Log(
+        GURL::EmptyGURL(), task_id, method_name,
+        actor::JournalDetailsBuilder()
+            .AddError("Task ID does not match current task")
+            .Add("expected_task_id", current_task_id_.value())
+            .Add("provided_task_id", task_id.value())
+            .Build());
+    return false;
+  }
+  return true;
+}
+
 actor::ActorKeyedService& GlicActorClientSession::actor_keyed_service() const {
   return *manager_->actor_keyed_service_;
 }
diff --git a/chrome/browser/glic/actor/glic_actor_task_manager.h b/chrome/browser/glic/actor/glic_actor_task_manager.h
index a5e97d4..0e86137 100644
--- a/chrome/browser/glic/actor/glic_actor_task_manager.h
+++ b/chrome/browser/glic/actor/glic_actor_task_manager.h
@@ -256,6 +256,8 @@
   void NotifyActorTaskStateChanged(actor::ActorTask& task);
   void StopTaskImpl(actor::TaskId task_id,
                     actor::ActorTask::StoppedReason reason);
+  bool ValidateTaskIdMatchesCurrent(actor::TaskId task_id,
+                                    std::string_view method_name);
   actor::ActorKeyedService& actor_keyed_service() const;
   GlicActorPolicyChecker& actor_policy_checker() const;
   GlicInstanceMetrics& instance_metrics() const;
diff --git a/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc b/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
index 909fe2a7..3f8990d 100644
--- a/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
+++ b/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
@@ -258,47 +258,47 @@
 
 IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                        PerformConcurrentAsyncWaitActions) {
-  // Manually create tasks via ActorKeyedService.
-  TaskId task_id_1 = actor_keyed_service()->CreateTask(
-      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());
-  TaskId task_id_2 = actor_keyed_service()->CreateTask(
-      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  ASSERT_NE(task_id, TaskId());
 
-  // Create tabs for each task using CreateActorTab API to ensure a
+  // Create tabs for the task using CreateActorTab API to ensure a
   // TabObservation is included in its result.
   ASSERT_OK_AND_ASSIGN(
       tabs::TabHandle tab_1,
-      CreateActorTab(task_id_1, /*open_in_background=*/false,
+      CreateActorTab(task_id, /*open_in_background=*/false,
                      base::ToString(active_tab()->GetHandle().raw_value()),
                      base::ToString(browser()->GetSessionID().id())));
   ASSERT_OK_AND_ASSIGN(
       tabs::TabHandle tab_2,
-      CreateActorTab(task_id_2, /*open_in_background=*/false,
+      CreateActorTab(task_id, /*open_in_background=*/false,
                      base::ToString(active_tab()->GetHandle().raw_value()),
                      base::ToString(browser()->GetSessionID().id())));
 
-  // Perform two WaitActions where the first resolves after the second
-  Actions action_1 = ::actor::MakeWait(kShortWaitTime * 2, tab_1, task_id_1);
-  Actions action_2 = ::actor::MakeWait(kShortWaitTime, tab_2, task_id_2);
-
+  // Perform two WaitActions
+  Actions action_1 = ::actor::MakeWait(kShortWaitTime, tab_1, task_id);
   std::unique_ptr<AsyncActionWaiter> waiter_1 = PerformActionsAsync(action_1);
-  std::unique_ptr<AsyncActionWaiter> waiter_2 = PerformActionsAsync(action_2);
-
-  // We should still be able to wait for result_2 after result_1 despite
-  // action_2 resolving first.
   ASSERT_OK_AND_ASSIGN(ActionsResult result_1, waiter_1->Wait());
+
+  Actions action_2 = ::actor::MakeWait(kShortWaitTime, tab_2, task_id);
+  std::unique_ptr<AsyncActionWaiter> waiter_2 = PerformActionsAsync(action_2);
   ASSERT_OK_AND_ASSIGN(ActionsResult result_2, waiter_2->Wait());
 
-  // Verify a tab observation was included in the results.
+  // Verify tab observations were included in the results.
   EXPECT_THAT(result_1, HasResultCode(::actor::mojom::ActionResultCode::kOk));
-  EXPECT_THAT(result_1.tabs(), testing::SizeIs(1));
+  EXPECT_THAT(result_1.tabs(), testing::SizeIs(2));
   EXPECT_THAT(result_1.tabs().at(0).result(),
               TabObservation::TAB_OBSERVATION_OK);
+  EXPECT_THAT(result_1.tabs().at(1).result(),
+              TabObservation::TAB_OBSERVATION_OK);
 
   EXPECT_THAT(result_2, HasResultCode(::actor::mojom::ActionResultCode::kOk));
-  EXPECT_THAT(result_2.tabs(), testing::SizeIs(1));
+  EXPECT_THAT(result_2.tabs(), testing::SizeIs(2));
   EXPECT_THAT(result_2.tabs().at(0).result(),
               TabObservation::TAB_OBSERVATION_OK);
+  EXPECT_THAT(result_2.tabs().at(1).result(),
+              TabObservation::TAB_OBSERVATION_OK);
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
 }
 
 IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
@@ -411,5 +411,68 @@
             ActorTask::State::kCreated);
 }
 
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       PerformActionsRejectsMismatchedTaskId) {
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  EXPECT_NE(task_id, TaskId());
+
+  const GURL target_url = embedded_https_test_server().GetURL(
+      "example.com", "/actor/blank.html?target");
+  // Construct actions targeting a different/foreign task ID.
+  TaskId foreign_task_id(task_id.value() + 999);
+  Actions action = ::actor::MakeNavigate(active_tab()->GetHandle(),
+                                         target_url.spec(), foreign_task_id);
+
+  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(action));
+  EXPECT_THAT(result,
+              HasResultCode(::actor::mojom::ActionResultCode::kTaskWentAway));
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
+}
+
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       CancelActionsRejectsMismatchedTaskId) {
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  EXPECT_NE(task_id, TaskId());
+
+  TaskId foreign_task_id(task_id.value() + 999);
+  EXPECT_THAT(
+      CancelActions(foreign_task_id),
+      base::test::ValueIs(glic::mojom::CancelActionsResult::kTaskNotFound));
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
+}
+
+// Security regression test for bug 5:
+// Verifies that a Glic guest cannot hijack a foreign task in the profile
+// (e.g. created by experimental_actor) when the Glic session has no active
+// task (current_task_id_ is null).
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       PerformActionsRejectsForeignTaskWhenNoCurrentTask) {
+  const TaskId victim_task_id = actor_keyed_service()->CreateTask(
+      ::actor::TaskSourceInfo(
+          ::actor::TaskSourceInfo::Client::kExperimentalActor,
+          /*id=*/std::nullopt),
+      ::actor::NoEnterprisePolicyChecker());
+  ASSERT_FALSE(victim_task_id.is_null());
+  ActorTask* victim_task = actor_keyed_service()->GetTask(victim_task_id);
+  ASSERT_NE(victim_task, nullptr);
+
+  const GURL initial_url = web_contents()->GetURL();
+  const GURL target_url = embedded_https_test_server().GetURL(
+      "example.com", "/actor/blank.html?hijacked-by-glic-guest");
+  ASSERT_NE(target_url, initial_url);
+
+  Actions hijack = ::actor::MakeNavigate(active_tab()->GetHandle(),
+                                         target_url.spec(), victim_task_id);
+
+  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(hijack));
+  EXPECT_THAT(result,
+              HasResultCode(::actor::mojom::ActionResultCode::kTaskWentAway));
+
+  EXPECT_EQ(initial_url, web_contents()->GetURL());
+  EXPECT_EQ(ActorTask::State::kCreated, victim_task->GetState());
+}
+
 }  // namespace
 }  // namespace glic::actor
diff --git a/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc b/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
index e6c5b88..3ba7feb 100644
--- a/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
+++ b/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
@@ -472,8 +472,9 @@
   // Pausing an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to pause task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "PauseActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -500,8 +501,9 @@
   // Pausing an inactive task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to pause task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "PauseActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -551,8 +553,9 @@
   // Interrupting an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to interrupt task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "InterruptActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -560,8 +563,9 @@
   // Uninterrupting an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to uninterrupt task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "UninterruptActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc b/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
index 909fe2a7..3f8990d 100644
--- a/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
+++ b/chrome/browser/glic/actor/glic_actor_action_execution_browsertest.cc
@@ -258,47 +258,47 @@
 
 IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                        PerformConcurrentAsyncWaitActions) {
-  // Manually create tasks via ActorKeyedService.
-  TaskId task_id_1 = actor_keyed_service()->CreateTask(
-      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());
-  TaskId task_id_2 = actor_keyed_service()->CreateTask(
-      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  ASSERT_NE(task_id, TaskId());
 
-  // Create tabs for each task using CreateActorTab API to ensure a
+  // Create tabs for the task using CreateActorTab API to ensure a
   // TabObservation is included in its result.
   ASSERT_OK_AND_ASSIGN(
       tabs::TabHandle tab_1,
-      CreateActorTab(task_id_1, /*open_in_background=*/false,
+      CreateActorTab(task_id, /*open_in_background=*/false,
                      base::ToString(active_tab()->GetHandle().raw_value()),
                      base::ToString(browser()->GetSessionID().id())));
   ASSERT_OK_AND_ASSIGN(
       tabs::TabHandle tab_2,
-      CreateActorTab(task_id_2, /*open_in_background=*/false,
+      CreateActorTab(task_id, /*open_in_background=*/false,
                      base::ToString(active_tab()->GetHandle().raw_value()),
                      base::ToString(browser()->GetSessionID().id())));
 
-  // Perform two WaitActions where the first resolves after the second
-  Actions action_1 = ::actor::MakeWait(kShortWaitTime * 2, tab_1, task_id_1);
-  Actions action_2 = ::actor::MakeWait(kShortWaitTime, tab_2, task_id_2);
-
+  // Perform two WaitActions
+  Actions action_1 = ::actor::MakeWait(kShortWaitTime, tab_1, task_id);
   std::unique_ptr<AsyncActionWaiter> waiter_1 = PerformActionsAsync(action_1);
-  std::unique_ptr<AsyncActionWaiter> waiter_2 = PerformActionsAsync(action_2);
-
-  // We should still be able to wait for result_2 after result_1 despite
-  // action_2 resolving first.
   ASSERT_OK_AND_ASSIGN(ActionsResult result_1, waiter_1->Wait());
+
+  Actions action_2 = ::actor::MakeWait(kShortWaitTime, tab_2, task_id);
+  std::unique_ptr<AsyncActionWaiter> waiter_2 = PerformActionsAsync(action_2);
   ASSERT_OK_AND_ASSIGN(ActionsResult result_2, waiter_2->Wait());
 
-  // Verify a tab observation was included in the results.
+  // Verify tab observations were included in the results.
   EXPECT_THAT(result_1, HasResultCode(::actor::mojom::ActionResultCode::kOk));
-  EXPECT_THAT(result_1.tabs(), testing::SizeIs(1));
+  EXPECT_THAT(result_1.tabs(), testing::SizeIs(2));
   EXPECT_THAT(result_1.tabs().at(0).result(),
               TabObservation::TAB_OBSERVATION_OK);
+  EXPECT_THAT(result_1.tabs().at(1).result(),
+              TabObservation::TAB_OBSERVATION_OK);
 
   EXPECT_THAT(result_2, HasResultCode(::actor::mojom::ActionResultCode::kOk));
-  EXPECT_THAT(result_2.tabs(), testing::SizeIs(1));
+  EXPECT_THAT(result_2.tabs(), testing::SizeIs(2));
   EXPECT_THAT(result_2.tabs().at(0).result(),
               TabObservation::TAB_OBSERVATION_OK);
+  EXPECT_THAT(result_2.tabs().at(1).result(),
+              TabObservation::TAB_OBSERVATION_OK);
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
 }
 
 IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
@@ -411,5 +411,68 @@
             ActorTask::State::kCreated);
 }
 
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       PerformActionsRejectsMismatchedTaskId) {
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  EXPECT_NE(task_id, TaskId());
+
+  const GURL target_url = embedded_https_test_server().GetURL(
+      "example.com", "/actor/blank.html?target");
+  // Construct actions targeting a different/foreign task ID.
+  TaskId foreign_task_id(task_id.value() + 999);
+  Actions action = ::actor::MakeNavigate(active_tab()->GetHandle(),
+                                         target_url.spec(), foreign_task_id);
+
+  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(action));
+  EXPECT_THAT(result,
+              HasResultCode(::actor::mojom::ActionResultCode::kTaskWentAway));
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
+}
+
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       CancelActionsRejectsMismatchedTaskId) {
+  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
+  EXPECT_NE(task_id, TaskId());
+
+  TaskId foreign_task_id(task_id.value() + 999);
+  EXPECT_THAT(
+      CancelActions(foreign_task_id),
+      base::test::ValueIs(glic::mojom::CancelActionsResult::kTaskNotFound));
+
+  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
+}
+
+// Security regression test for bug 5:
+// Verifies that a Glic guest cannot hijack a foreign task in the profile
+// (e.g. created by experimental_actor) when the Glic session has no active
+// task (current_task_id_ is null).
+IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
+                       PerformActionsRejectsForeignTaskWhenNoCurrentTask) {
+  const TaskId victim_task_id = actor_keyed_service()->CreateTask(
+      ::actor::TaskSourceInfo(
+          ::actor::TaskSourceInfo::Client::kExperimentalActor,
+          /*id=*/std::nullopt),
+      ::actor::NoEnterprisePolicyChecker());
+  ASSERT_FALSE(victim_task_id.is_null());
+  ActorTask* victim_task = actor_keyed_service()->GetTask(victim_task_id);
+  ASSERT_NE(victim_task, nullptr);
+
+  const GURL initial_url = web_contents()->GetURL();
+  const GURL target_url = embedded_https_test_server().GetURL(
+      "example.com", "/actor/blank.html?hijacked-by-glic-guest");
+  ASSERT_NE(target_url, initial_url);
+
+  Actions hijack = ::actor::MakeNavigate(active_tab()->GetHandle(),
+                                         target_url.spec(), victim_task_id);
+
+  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(hijack));
+  EXPECT_THAT(result,
+              HasResultCode(::actor::mojom::ActionResultCode::kTaskWentAway));
+
+  EXPECT_EQ(initial_url, web_contents()->GetURL());
+  EXPECT_EQ(ActorTask::State::kCreated, victim_task->GetState());
+}
+
 }  // namespace
 }  // namespace glic::actor
diff --git a/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc b/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
index e6c5b88..3ba7feb 100644
--- a/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
+++ b/chrome/browser/glic/actor/glic_actor_task_lifecycle_browsertest.cc
@@ -472,8 +472,9 @@
   // Pausing an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to pause task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "PauseActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -500,8 +501,9 @@
   // Pausing an inactive task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to pause task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "PauseActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -551,8 +553,9 @@
   // Interrupting an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to interrupt task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "InterruptActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 
   ContinueJsTest();
@@ -560,8 +563,9 @@
   // Uninterrupting an invalid task should be a no-op and log an error.
   observer.WaitUntil(
       base::BindRepeating([](const ::actor::mojom::JournalEntry& entry) {
-        return entry.event == "Failed to uninterrupt task" &&
-               JournalEntryHasError(entry, "No such task");
+        return entry.event == "UninterruptActorTask" &&
+               JournalEntryHasError(entry,
+                                    "Task ID does not match current task");
       }));
 }
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.