ax: Scope automation action callbacks with random IDs to prevent hijacking [chromium/src : main]

0 views
Skip to first unread message

Hongyu Long (Gerrit)

unread,
May 26, 2026, 7:39:44 PM (6 days ago) May 26
to Katie D, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org
Attention needed from Katie D

Hongyu Long added 1 comment

Patchset-level comments
File-level comment, Patchset 1:
Katie D . resolved

It looks like this was made global to fix a bug:
https://issues.chromium.org/issues/41374256, https://chromium-review.git.corp.google.com/c/chromium/src/+/1074490

Can you check whether the bug will regress if you add this change? That change only had manual testing.

Hongyu Long
Thank you for the excellent catch, Katie! You are 100% correct.

I verified the regression by running the existing `HitTestMultipleWindows` test (which uses `desktop.hitTestWithReply`), and indeed it timed out/failed with the previous per-tree map fix. This happens because the action is registered on the Desktop tree but the response event is processed on the child web page's tree (re-triggering the bug fixed in CL 1074490).

To resolve this without regressing cross-tree hit testing, I have updated the CL with a much more robust approach:
1. We keep the callback map global static so that cross-tree resolution continues to work as intended.
2. Instead of using sequential integer request IDs (which are easily guessable), we now generate a random 31-bit positive integer in JS for each request.

Since a compromised renderer cannot read the privileged extension's memory, it cannot predict or guess the random IDs, making it impossible to forge events for other trees (effectively neutralizing the hijacking vector).

I have verified that with this new fix:
- `HitTestMultipleWindows` (the regressed test) now passes cleanly in ~3.4s.
- `PreventCrossTreeCallbackHijack` (our security test) still passes cleanly and successfully blocks forged hijack events.

I have amended the CL with these changes. Please take another look!
Open in Gerrit

Related details

Attention is currently required from:
  • Katie D
Submit Requirements:
  • requirement satisfiedCode-Coverage
  • requirement is not satisfiedCode-Owners
  • requirement is not satisfiedCode-Review
  • requirement is not satisfiedReview-Enforcement
Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
Gerrit-MessageType: comment
Gerrit-Project: chromium/src
Gerrit-Branch: main
Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
Gerrit-Change-Number: 7865727
Gerrit-PatchSet: 2
Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
Gerrit-Reviewer: Katie D <ka...@chromium.org>
Gerrit-CC: Erol Bicioglu <bici...@chromium.org>
Gerrit-Attention: Katie D <ka...@chromium.org>
Gerrit-Comment-Date: Tue, 26 May 2026 23:39:34 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Katie D <ka...@chromium.org>
satisfied_requirement
unsatisfied_requirement
open
diffy

Katie D (Gerrit)

unread,
May 27, 2026, 2:33:42 PM (5 days ago) May 27
to Hongyu Long, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org
Attention needed from Hongyu Long

Katie D added 9 comments

File chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
Line 7, Patchset 2 (Latest):function findNodePageWide(node, predicate) {
Katie D . unresolved

why can't we use rootNode.find instead of this custom search?

Line 40, Patchset 2 (Latest): const textField = await new Promise((resolve, reject) => {
Katie D . unresolved

can we run this without polling, maybe using some listeners instead? see for example the way we wait for load complete in chrome/test/data/extensions/api_test/automation/tests/desktop/hit_test.js.

Line 44, Patchset 2 (Latest): }, 15000); // 15s timeout
Katie D . unresolved

this is too long for a browsertest.

Line 46, Patchset 2 (Latest): const interval = setInterval(() => {
Katie D . unresolved

also better if we can do this without polling.

File extensions/renderer/resources/automation/automation_node.js
Line 1756, Patchset 2 (Latest): * secured by cryptographically unpredictable random request IDs.
Katie D . unresolved

maybe "Request IDs are randomized to avoid cross-tree hijacking" or just "Request IDs are random numbers."

Line 1968, Patchset 2 (Latest): // Use a random 31-bit positive integer for the request ID.
// This makes the ID completely unpredictable to a compromised renderer,
// preventing cross-tree callback hijacking.
Katie D . unresolved

i don't think we need this comment.

Line 1971, Patchset 2 (Latest): requestID = Math.floor(Math.random() * 2147483647);
Katie D . unresolved

where does this number come from? should it be a constant or something?

Line 1972, Patchset 2 (Latest): } while (requestID in map);
Katie D . unresolved

AI suggestion:

Since you are using map just to check for the existence of keys, your current code is totally fine. However, using requestID in map will check the object's entire prototype chain.

A slightly safer and faster modern approach for checking keys in a plain object is using Object.hasOwn():

```
} while (Object.hasOwn(map, requestID));
```

Line 1985, Patchset 2 (Latest): if (requestID in map) {
Katie D . unresolved

just curious why we don't use AutomationRootNodeImpl.actionRequestIDToCallback directly, as we did before this patch set?

Open in Gerrit

Related details

Attention is currently required from:
  • Hongyu Long
Submit Requirements:
    • requirement satisfiedCode-Coverage
    • requirement is not satisfiedCode-Owners
    • requirement is not satisfiedCode-Review
    • requirement is not satisfiedNo-Unresolved-Comments
    • requirement is not satisfiedReview-Enforcement
    Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
    Gerrit-MessageType: comment
    Gerrit-Project: chromium/src
    Gerrit-Branch: main
    Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
    Gerrit-Change-Number: 7865727
    Gerrit-PatchSet: 2
    Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
    Gerrit-Reviewer: Katie D <ka...@chromium.org>
    Gerrit-CC: Erol Bicioglu <bici...@chromium.org>
    Gerrit-Attention: Hongyu Long <hongy...@chromium.org>
    Gerrit-Comment-Date: Wed, 27 May 2026 18:33:29 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    satisfied_requirement
    unsatisfied_requirement
    open
    diffy

    Hongyu Long (Gerrit)

    unread,
    May 27, 2026, 5:23:37 PM (5 days ago) May 27
    to Katie D, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org
    Attention needed from Katie D

    Hongyu Long added 10 comments

    Patchset-level comments
    File-level comment, Patchset 3 (Latest):
    Hongyu Long . resolved

    Thank you Katie! Please take another look!

    File chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
    Line 7, Patchset 2:function findNodePageWide(node, predicate) {
    Katie D . resolved

    why can't we use rootNode.find instead of this custom search?

    Hongyu Long

    Done. Remove `findNodePageWide` completely. Use `rootNode.find` directly with a predicate to locate the correct text field inside the Victim tab (distinguishing it from the browser's Omnibox).

    Line 40, Patchset 2: const textField = await new Promise((resolve, reject) => {
    Katie D . resolved

    can we run this without polling, maybe using some listeners instead? see for example the way we wait for load complete in chrome/test/data/extensions/api_test/automation/tests/desktop/hit_test.js.

    Hongyu Long

    Done. Thanks for the suggestion.

    Line 44, Patchset 2: }, 15000); // 15s timeout
    Katie D . resolved

    this is too long for a browsertest.

    Hongyu Long

    Done. The timeout has been removed along with the polling loop.

    Line 46, Patchset 2: const interval = setInterval(() => {
    Katie D . resolved

    also better if we can do this without polling.

    Hongyu Long

    Done

    File extensions/renderer/resources/automation/automation_node.js
    Line 1756, Patchset 2: * secured by cryptographically unpredictable random request IDs.
    Katie D . resolved

    maybe "Request IDs are randomized to avoid cross-tree hijacking" or just "Request IDs are random numbers."

    Hongyu Long

    Done

    Line 1968, Patchset 2: // Use a random 31-bit positive integer for the request ID.

    // This makes the ID completely unpredictable to a compromised renderer,
    // preventing cross-tree callback hijacking.
    Katie D . resolved

    i don't think we need this comment.

    Hongyu Long

    Done

    Line 1971, Patchset 2: requestID = Math.floor(Math.random() * 2147483647);
    Katie D . resolved

    where does this number come from? should it be a constant or something?

    Hongyu Long

    Good point. I have defined `const INT32_MAX = 2147483647`; at the top of the file and used it here to make it self-documenting.

    Line 1972, Patchset 2: } while (requestID in map);
    Katie D . resolved

    AI suggestion:

    Since you are using map just to check for the existence of keys, your current code is totally fine. However, using requestID in map will check the object's entire prototype chain.

    A slightly safer and faster modern approach for checking keys in a plain object is using Object.hasOwn():

    ```
    } while (Object.hasOwn(map, requestID));
    ```

    Hongyu Long

    Done

    Line 1985, Patchset 2: if (requestID in map) {
    Katie D . resolved

    just curious why we don't use AutomationRootNodeImpl.actionRequestIDToCallback directly, as we did before this patch set?

    Hongyu Long

    You are right, keeping it direct is more consistent with the rest of the file. I have removed the local map alias in `onGetTextLocationResult` and `onActionResult` and reverted to using it directly.

    Open in Gerrit

    Related details

    Attention is currently required from:
    • Katie D
    Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement is not satisfiedCode-Owners
      • requirement is not satisfiedCode-Review
      • requirement is not satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
      Gerrit-Change-Number: 7865727
      Gerrit-PatchSet: 3
      Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
      Gerrit-Reviewer: Katie D <ka...@chromium.org>
      Gerrit-CC: Erol Bicioglu <bici...@chromium.org>
      Gerrit-Attention: Katie D <ka...@chromium.org>
      Gerrit-Comment-Date: Wed, 27 May 2026 21:23:25 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: No
      Comment-In-Reply-To: Katie D <ka...@chromium.org>
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Katie D (Gerrit)

      unread,
      May 27, 2026, 5:50:01 PM (5 days ago) May 27
      to Hongyu Long, Chromium LUCI CQ, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org
      Attention needed from Hongyu Long

      Katie D voted and added 3 comments

      Votes added by Katie D

      Code-Review+1

      3 comments

      Patchset-level comments
      Katie D . resolved

      LGTM % failing presubmits and small comments.

      File chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
      Line 8, Patchset 3 (Latest): // Robustly find the text field inside the Victim tab's tree,
      // waiting for the tree to load if necessary using listeners (no polling).
      Katie D . unresolved

      Can you remove this or just make it "find the text field inside the Victim tab's tree"?

      Line 38, Patchset 3 (Latest): console.log('Found Victim text field:', textField.toString());
      Katie D . unresolved

      can remove console.log here and below

      Open in Gerrit

      Related details

      Attention is currently required from:
      • Hongyu Long
      Submit Requirements:
      • requirement satisfiedCode-Coverage
      • requirement satisfiedCode-Owners
      • requirement satisfiedCode-Review
      • requirement is not satisfiedNo-Unresolved-Comments
      • requirement satisfiedReview-Enforcement
      Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
      Gerrit-MessageType: comment
      Gerrit-Project: chromium/src
      Gerrit-Branch: main
      Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
      Gerrit-Change-Number: 7865727
      Gerrit-PatchSet: 3
      Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
      Gerrit-Reviewer: Hongyu Long <hongy...@chromium.org>
      Gerrit-Reviewer: Katie D <ka...@chromium.org>
      Gerrit-Attention: Hongyu Long <hongy...@chromium.org>
      Gerrit-Comment-Date: Wed, 27 May 2026 21:49:45 +0000
      Gerrit-HasComments: Yes
      Gerrit-Has-Labels: Yes
      satisfied_requirement
      unsatisfied_requirement
      open
      diffy

      Hongyu Long (Gerrit)

      unread,
      1:00 PM (4 hours ago) 1:00 PM
      to Katie D, Chromium LUCI CQ, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org

      Hongyu Long added 2 comments

      File chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
      Line 8, Patchset 3: // Robustly find the text field inside the Victim tab's tree,

      // waiting for the tree to load if necessary using listeners (no polling).
      Katie D . resolved

      Can you remove this or just make it "find the text field inside the Victim tab's tree"?

      Hongyu Long

      Done

      Line 38, Patchset 3: console.log('Found Victim text field:', textField.toString());
      Katie D . resolved

      can remove console.log here and below

      Hongyu Long

      Done

      Open in Gerrit

      Related details

      Attention set is empty
      Submit Requirements:
        • requirement satisfiedCode-Coverage
        • requirement satisfiedCode-Owners
        • requirement satisfiedCode-Review
        • requirement satisfiedReview-Enforcement
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
        Gerrit-Change-Number: 7865727
        Gerrit-PatchSet: 5
        Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Katie D <ka...@chromium.org>
        Gerrit-CC: Erol Bicioglu <bici...@chromium.org>
        Gerrit-Comment-Date: Mon, 01 Jun 2026 17:00:04 +0000
        satisfied_requirement
        open
        diffy

        Hongyu Long (Gerrit)

        unread,
        1:00 PM (4 hours ago) 1:00 PM
        to Katie D, Chromium LUCI CQ, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org

        Hongyu Long voted Commit-Queue+2

        Commit-Queue+2
        Open in Gerrit

        Related details

        Attention set is empty
        Submit Requirements:
        • requirement satisfiedCode-Coverage
        • requirement satisfiedCode-Owners
        • requirement satisfiedCode-Review
        • requirement satisfiedReview-Enforcement
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: comment
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
        Gerrit-Change-Number: 7865727
        Gerrit-PatchSet: 5
        Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Katie D <ka...@chromium.org>
        Gerrit-CC: Erol Bicioglu <bici...@chromium.org>
        Gerrit-Comment-Date: Mon, 01 Jun 2026 17:00:09 +0000
        Gerrit-HasComments: No
        Gerrit-Has-Labels: Yes
        satisfied_requirement
        open
        diffy

        Chromium LUCI CQ (Gerrit)

        unread,
        2:22 PM (3 hours ago) 2:22 PM
        to Hongyu Long, Katie D, Erol Bicioglu, android-bu...@system.gserviceaccount.com, extension...@chromium.org, chromium-a...@chromium.org

        Chromium LUCI CQ submitted the change with unreviewed changes

        Unreviewed changes

        3 is the latest approved patch-set.
        The change was submitted with unreviewed changes in the following files:

        ```
        The name of the file: chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
        Insertions: 2, Deletions: 6.

        @@ -2,10 +2,8 @@
        // Use of this source code is governed by a BSD-style license that can be
        // found in the LICENSE file.

        -let allTests = [async function testActionResultHijack() {
        - console.log('Starting testActionResultHijack');
        -
        - // Robustly find the text field inside the Victim tab's tree,
        +const allTests = [async function testActionResultHijack() {
        + // Find the text field inside the Victim tab's tree,

        // waiting for the tree to load if necessary using listeners (no polling).
           const textField = await new Promise(resolve => {
        const isVictimRoot = (node) => {
        @@ -35,7 +33,6 @@
        });

        assertTrue(!!textField);
        - console.log('Found Victim text field:', textField.toString());

        // Perform scrollBackward.
        textField.scrollBackward(result => {
        @@ -44,7 +41,6 @@
        // result = true on the victim tree) will resolve this callback.
        // If vulnerable, the forged event will hijack the callback and we will
        // get 'false' here, causing the assertion to fail.
        - console.log('scrollBackward callback invoked with result:', result);
        assertTrue(result);
        chrome.test.succeed();
        });
        ```

        Change information

        Commit message:
        ax: Scope automation action callbacks with random IDs to prevent hijacking

        Currently, AutomationRootNodeImpl tracks pending asynchronous action
        callbacks (like hitTest or scroll) in a global, class-static map
        (actionRequestIDToCallback) keyed by a sequential integer request ID.

        A compromised renderer can guess these sequential IDs and send forged
        AXEvents (e.g. kHitTestResult) to the browser, which are forwarded to
        the automation extension. Because the lookup map is global, the forged
        event can resolve and consume a legitimate callback registered by a
        different tree (like the Desktop tree used by ChromeVox), enabling
        cross-tree callback hijacking (e.g., UI spoofing or touch coordinate leaks).

        We previously attempted to fix this by scoping the callback map to
        individual tree instances. However, this regresses legitimate cross-tree
        hit testing (like desktop.hitTestWithReply), where the action is
        initiated on the Desktop tree but the response event is processed on
        the target web page's tree (CL 1074490).

        This CL resolves the vulnerability while preserving cross-tree hit
        testing by keeping the map global but securing it with cryptographically
        unpredictable random 31-bit integer request IDs. Since a compromised

        renderer cannot read the privileged extension's memory, it cannot
        guess or predict the IDs to forge events, effectively neutralizing
        the hijacking vector.

        We also add a robust integration test suite:
        - C++ test (PreventCrossTreeCallbackHijack) that simulates the compromised
        renderer by injecting a forged event from a different tree ID.
        - JS test (action_result_hijack.js) that performs the action and verifies
        the forged event is ignored while the real event resolves the callback.
        AX-Relnotes: n/a.
        Bug: 513720407
        Test: PreventCrossTreeCallbackHijack, HitTestMultipleWindows
        Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
        Commit-Queue: Hongyu Long <hongy...@chromium.org>
        Reviewed-by: Katie D <ka...@chromium.org>
        Cr-Commit-Position: refs/heads/main@{#1639539}
        Files:
        • M chrome/browser/extensions/api/automation/automation_apitest.cc
        • A chrome/test/data/extensions/api_test/automation/tests/desktop/action_result_hijack.js
        • M extensions/renderer/resources/automation/automation_node.js
        Change size: M
        Delta: 3 files changed, 172 insertions(+), 13 deletions(-)
        Branch: refs/heads/main
        Submit Requirements:
        • requirement satisfiedCode-Review: +1 by Katie D
        Open in Gerrit
        Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. DiffyGerrit
        Gerrit-MessageType: merged
        Gerrit-Project: chromium/src
        Gerrit-Branch: main
        Gerrit-Change-Id: Ieb354da6e3a2b5c1c05b2ea3c2abbd7178efe8ba
        Gerrit-Change-Number: 7865727
        Gerrit-PatchSet: 6
        Gerrit-Owner: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Chromium LUCI CQ <chromiu...@luci-project-accounts.iam.gserviceaccount.com>
        Gerrit-Reviewer: Hongyu Long <hongy...@chromium.org>
        Gerrit-Reviewer: Katie D <ka...@chromium.org>
        open
        diffy
        satisfied_requirement
        Reply all
        Reply to author
        Forward
        0 new messages