Open Bug 2023093 Opened 7 days ago Updated 1 day ago

WindowContext::CanSet for UserActivationStateAndModifiers missing ownership check allows cross-process activation injection

Categories

(Core :: DOM: Core & HTML, defect)

defect

Tracking

()

People

(Reporter: lukefrancis09, Unassigned)

References

(Blocks 1 open bug)

Details

(Keywords: ai-involved, reporter-external, Whiteboard: [client-bounty-form])

WindowContext::CanSet for UserActivationStateAndModifiers returns true unconditionally, unlike 13 other security-sensitive WindowContext fields that use CheckOnlyOwningProcessCanSet. A compromised content process can inject user activation into any WindowContext within its BrowsingContextGroup, bypassing the user gesture requirement for fullscreen, clipboard access, pointer lock, display capture, storage access, autoplay, and other activation-gated APIs — in a different content process's window, without any user interaction.

The Bug

docshell/base/WindowContext.h:345-350:

bool CanSet(FieldIndex<IDX_UserActivationStateAndModifiers>,
            const UserActivation::StateAndModifiers::DataT&,
            ContentParent* aSource) {
  return true;
}

The aSource parameter (the sending ContentParent) is ignored. Compare with 13 other fields in the same file that enforce ownership:

IDX_IsSecure, IDX_CookieBehavior, IDX_IsOnContentBlockingAllowList, IDX_IsThirdPartyWindow, IDX_IsThirdPartyTrackingResourceWindow, IDX_UsingStorageAccess, IDX_ShouldResistFingerprinting, IDX_IsSecureContext, IDX_IsOriginalFrameSource, IDX_AutoplayPermission, IDX_PopupPermission, IDX_NeedsBeforeUnload, IDX_NeedsTraverse

All use CheckOnlyOwningProcessCanSet(aSource) (WindowContext.cpp:226-236), which verifies Canonical()->GetContentParent() == aSource. UserActivationStateAndModifiers skips this entirely.

The Exploit Path

ContentParent::RecvCommitWindowContextTransaction (ContentParent.cpp:7805) receives a MaybeDiscardedWindowContext resolved by global ID — any WindowContext in the sender's BrowsingContextGroup is reachable. The sole validation gate is CanSet, which returns true.

When the transaction is forwarded to the owning content process and applied, DidSet (WindowContext.cpp:443-459) runs:

if (GetUserActivationState() == UserActivation::State::FullActivated) {
  mLastActivationTimestamp = TimeStamp::Now();
}

This sets a real timestamp, causing HasValidTransientUserGestureActivation() (WindowContext.cpp:590-615) to return true. The injected activation is indistinguishable from a real user gesture.

APIs Gated by Transient User Activation

All check HasValidTransientUserGestureActivation() on the synced WindowContext field:

API Location
Fullscreen Element.cpp:4712
Clipboard read dom/events/Clipboard.cpp:367
Pointer Lock dom/base/PointerLockManager.cpp:202
Storage Access dom/base/Document.cpp:19797
Autoplay dom/media/autoplay/AutoplayPolicy.cpp
Display Capture dom/media/MediaDevices.cpp:527
selectAudioOutput dom/media/MediaDevices.cpp:658
execCommand paste dom/base/Document.cpp:5739

Attack Scenario

  1. Compromised content process A sends CommitWindowContextTransaction targeting WindowContext W owned by content process B (same BrowsingContextGroup — e.g., opener relationship or iframe)
  2. Transaction sets UserActivationStateAndModifiers to FullActivated (value 2)
  3. Parent accepts (CanSet returns true), forwards to process B
  4. Process B applies the transaction, DidSet fires, mLastActivationTimestamp = TimeStamp::Now()
  5. Scripts in process B's window now pass all transient activation checks without any real user gesture
  6. Process B can enter fullscreen, read clipboard, acquire pointer lock, capture display, access storage, autoplay media

Scope: same BrowsingContextGroup only (opener relationships, same-site iframes). Cross-group targeting is not possible.

Suggested Fix

One-line change in docshell/base/WindowContext.h:345-350:

 bool CanSet(FieldIndex<IDX_UserActivationStateAndModifiers>,
             const UserActivation::StateAndModifiers::DataT&,
             ContentParent* aSource) {
-  return true;
+  return CheckOnlyOwningProcessCanSet(aSource);
 }

Consistent with how the 13 other security-sensitive fields in the same file are protected.

IDX_EmbedderPolicy (WindowContext.h:305), IDX_SHEntryHasUserInteraction (335), IDX_HasActiveCloseWatcher (375) also return true unconditionally and may warrant the same fix.

Found on 148.0.2.

let me know if you need any additional information, happy to provide it!

Flags: sec-bounty?
Group: firefox-core-security → dom-core-security
Component: Security → DOM: Content Processes
Product: Firefox → Core
Component: DOM: Content Processes → DOM: Core & HTML

I'm not exactly sure if this is a security problem. My understanding is that the point of user activation is to prevent a page from doing something, so if hostile site A flips on activation for non-hostile site B, what does that get you? Maybe you can make it easier for site B to grab information and using some other site isolation issue it makes it easier for site A to get at it.

Also, I'm not an expert on user activation, but I'd kind of expect that everything in the same tab gets user activation at once anyways, but maybe that is wrong.

The strongest case that I can see is the interaction with SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION. A compromised embedder process can inject activation into a cross-origin sandboxed iframe (same BrowsingContextGroup) which would enable that iframe to navigate the top-level browsing context via BrowsingContext::IsSandboxedFrom (BrowsingContext.cpp:1625-1636). This bypasses the sandbox restriction specifically designed to prevent navigation without user gesture.

That said, I do recognize this is a pretty narrow scenario and I'd defer to your judgment on whether it crosses the security threshold. At minimum it seems like a correctness fix for consistency with the other 13 fields, so happy for it to land as a non-security bug if you think that's the right classification.

Thank you,

Luke

Duplicate of this bug: 2023429

If it's not too much trouble for you, could I get access to 2023429 to see what it was duped against? Thank you

Luke

(In reply to Luke Francis from comment #4)

If it's not too much trouble for you, could I get access to 2023429 to see what it was duped against? Thank you

Sure. done.

Hi,

it looks like this security bug was reported without a test case attachment.
As per our new guidelines, we no longer consider bugs without a test case as elegible for a bug bounty.

For example, a good test case for a memory corruption is an attached HTML file called "testcase.html" that reliably crashes Firefox in an address sanitizer build.

Please re-read our various pages here and submit a reproducible testcase as an attachment to the bug.

(This is an automated comment. Use the filter word "king-taste-soap" if you need to filter bulk email)

After thinking about how I would create a poc, I don't think this crosses the security threshold. The missing check is a real consistency bug, as it should use CheckOnlyOwningProcessCanSet like the other fields, but I can't construct a scenario where a compromised content process gains a concrete capability it didn't already have. Cross-process activation injection doesn't help the attacker because they can't execute code in the target process, and same-process exploitation doesn't need the IPC path. Happy for this to land as a non-security correctness fix. Thank you :)

Luke

Group: dom-core-security
Status: UNCONFIRMED → NEW
Ever confirmed: true
Flags: sec-bounty? → sec-bounty-
Duplicate of this bug: 2024575
No longer duplicate of this bug: 2024575
You need to log in before you can comment on or make changes to this bug.