CVE-2026-14020
Overview
Files Changed
content/browser/xr/service/vr_service_impl.cc
Patch
From 3b082d7458a9c692a550b0c04c6d7c7d1e8d769d Mon Sep 17 00:00:00 2001 From: Alexander Cooper <[email protected]> Date: Mon, 01 Jun 2026 17:16:38 -0700 Subject: [PATCH] [WebXR] Check page visibility during session request validation Validates that the requesting frame's visibility state is set to visible when processing a VRService::RequestSession Mojo call in VRServiceImpl, maintaining proper alignment with renderer-side session requirements. Fixed: 517598518 Change-Id: Ibcaafa8dbdbec3a53e556a9459d3658edc3ffa24 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7885957 Auto-Submit: Alexander Cooper <[email protected]> Commit-Queue: Alexander Cooper <[email protected]> Reviewed-by: Brandon Jones <[email protected]> Cr-Commit-Position: refs/heads/main@{#1639800} --- diff --git a/content/browser/xr/service/vr_service_impl.cc b/content/browser/xr/service/vr_service_impl.cc index eb9239c9..6a7add77 100644 --- a/content/browser/xr/service/vr_service_impl.cc +++ b/content/browser/xr/service/vr_service_impl.cc @@ -39,6 +39,7 @@ #include "content/public/browser/web_contents.h" #include "content/public/common/child_process_id_util.h" #include "content/public/common/origin_util.h" +#include "content/public/common/page_visibility_state.h" #include "device/vr/buildflags/buildflags.h" #include "device/vr/public/cpp/features.h" #include "device/vr/public/cpp/session_mode.h" @@ -566,6 +567,17 @@ return; } + if (render_frame_host_->GetVisibilityState() != + content::PageVisibilityState::kVisible) { + // Page visibility is verified blink-side, so this should never fail unless + // the requesting client is misbehaving or compromised. Treat non-visible + // page as unknown failure: + RejectSession(std::move(callback), options->trace_id, + device::mojom::RequestSessionError::UNKNOWN_FAILURE, + "Page is not visible."); + return; + } + // The consent flow cannot differentiate between optional and required // features, but we don't need to block creation if an optional feature is // not supported. Remove all unsupported optional features from the
Original Bug Report
Potential page-visibility bypass in VRServiceImpl allows background XR session takeover
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The browser-side WebXR implementation in VRServiceImpl::RequestSession fails to re-validate page visibility before starting an immersive session. While the Blink renderer checks both transient user activation and page visibility, the browser-side Mojo implementation only re-validates transient user activation. A compromised renderer can potentially exploit this gap to launch an immersive XR session from a background or hidden tab, leading to UI spoofing or origin confusion inside the headset.
Affected files:
content/browser/xr/service/vr_service_impl.ccthird_party/blink/renderer/modules/xr/xr_system.cc
Estimated timestamp from git blame: 2019-09-05
Summary
A defense-in-depth validation gap exists in the browser-side WebXR Mojo host implementation. The WebXR specification dictates that an immersive session request is only allowed if the requesting document is both active via transient user activation and visible (trustworthy). While the Blink renderer checks both requirements, VRServiceImpl::RequestSession in the browser process only validates the transient user activation state. A compromised renderer could potentially bypass the renderer-side visibility check and trigger an immersive session from a hidden or background tab, leading to physical headset takeover or full-screen UI spoofing.
Code Analysis
In the Blink renderer, both constraints are verified correctly in third_party/blink/renderer/modules/xr/xr_system.cc (lines 259-274):
const char* CheckImmersiveSessionRequestAllowed(LocalDOMWindow* window) {
if (!LocalFrame::HasTransientUserActivation(window->GetFrame())) {
return kRequestRequiresUserActivation;
}
// Check that the document is "trustworthy"
if (!window->document()->IsPageVisible()) {
return kPageNotVisible;
}
return nullptr;
}
However, in the browser process, the Mojo handler VRServiceImpl::RequestSession in content/browser/xr/service/vr_service_impl.cc (lines 556-567) only re-validates the transient user activation:
const bool has_user_activation =
render_frame_host_->HasTransientUserActivation();
if (!has_user_activation) {
RejectSession(std::move(callback), options->trace_id,
device::mojom::RequestSessionError::UNKNOWN_FAILURE,
"Missing user activation.");
return;
}
There is no validation check of the requesting frame’s visibility state (e.g., render_frame_host_->GetVisibilityState()), allowing a compromised renderer to directly request an immersive session even if the tab is hidden.
Potential Step-by-Step Attack Scenario
Note: These are potential steps based on source code analysis; our tooling agent currently lacks the ability to execute code or verify runtime behavior with a working exploit.
- The user visits an attacker-controlled origin and grants persistent WebXR/AR spatial tracking permissions.
- The user interacts with the page, activating transient user activation (which remains active for up to 5 seconds).
- The user switches to a different, trusted tab (e.g., a banking site) or minimizes the browser, transitioning the attacker’s tab to a hidden state (
content::PageVisibilityState::kHidden). - Within the active 5-second transient user activation window, the attacker leverages a separate renderer compromise (e.g., a V8 exploit) to execute shellcode in the renderer process.
- The shellcode bypasses Blink’s renderer-side checks in
xr_system.ccand directly calls the browser’sVRService::RequestSessionMojo interface. - Because the user activation is still valid in the browser process and permissions were previously granted (avoiding any consent prompt), the browser launches the immersive session on the active VR/AR device without verifying that the requesting page is visible.
- The physical VR/AR headset or phone screen launches the attacker’s immersive environment. Because this occurs while the user is actively viewing a different, trusted tab, the user may attribute the session takeover to the trusted foreground tab, creating an opportunity for convincing phishing and origin-spoofing attacks.
Suggested Fix
To align browser-side security checks with the renderer-side restrictions, VRServiceImpl::RequestSession should check that the requesting document is visible before proceeding with session creation, similar to other Mojo interface handlers like ImageCaptureImpl or FontAccessManager:
if (render_frame_host_->GetVisibilityState() !=
content::PageVisibilityState::kVisible) {
RejectSession(std::move(callback), options->trace_id,
device::mojom::RequestSessionError::UNKNOWN_FAILURE,
"Page is not visible.");
return;
}
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.