WindowContext::CanSet for UserActivationStateAndModifiers missing ownership check allows cross-process activation injection
Categories
(Core :: DOM: Core & HTML, 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
- Compromised content process A sends
CommitWindowContextTransactiontargeting WindowContext W owned by content process B (same BrowsingContextGroup — e.g., opener relationship or iframe) - Transaction sets
UserActivationStateAndModifierstoFullActivated(value 2) - Parent accepts (
CanSetreturnstrue), forwards to process B - Process B applies the transaction,
DidSetfires,mLastActivationTimestamp = TimeStamp::Now() - Scripts in process B's window now pass all transient activation checks without any real user gesture
- 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!
Updated•7 days ago
|
Updated•7 days ago
|
Updated•7 days ago
|
Updated•7 days ago
|
Comment 1•7 days ago
|
||
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.
| Reporter | ||
Comment 2•7 days ago
|
||
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
| Reporter | ||
Comment 4•5 days ago
|
||
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
Comment 5•4 days ago
|
||
(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.
Comment 6•4 days ago
|
||
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.
- https://www.mozilla.org/en-US/security/bug-bounty/
- https://www.mozilla.org/en-US/security/client-bug-bounty/
- https://www.mozilla.org/en-US/security/bug-bounty/faq/
- https://attackanddefense.dev/2026/03/13/bug-bounty-program-updates-2026.html
(This is an automated comment. Use the filter word "king-taste-soap" if you need to filter bulk email)
| Reporter | ||
Comment 7•4 days ago
|
||
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
Updated•3 days ago
|
Description
•