URL Spoofing by delaying a navigation and using the onbeforeunload dialog
Categories
(Core :: DOM: Navigation, defect, P2)
Tracking
()
People
(Reporter: luan.herrera, Unassigned, NeedInfo)
References
()
Details
(Keywords: csectype-spoof, reporter-external, sec-moderate, Whiteboard: [reporter-external] [client-bounty-form] [verif?][secdom:beforeunload])
Attachments
(2 files, 1 obsolete file)
Reporter | ||
Comment 1•6 years ago
|
||
Comment 2•6 years ago
|
||
Updated•6 years ago
|
Comment 3•6 years ago
|
||
Reporter | ||
Comment 4•6 years ago
|
||
Comment 5•6 years ago
|
||
Comment 6•6 years ago
|
||
Updated•6 years ago
|
Comment 7•6 years ago
|
||
Comment 8•6 years ago
|
||
Comment 9•6 years ago
|
||
Comment 10•6 years ago
|
||
Comment 11•6 years ago
|
||
Comment 12•6 years ago
|
||
Comment 13•6 years ago
|
||
Comment 14•6 years ago
|
||
Comment 15•6 years ago
|
||
Comment 16•6 years ago
|
||
Comment 17•6 years ago
|
||
Comment 18•6 years ago
|
||
Comment 19•6 years ago
|
||
Comment 20•6 years ago
|
||
Comment 21•6 years ago
|
||
Comment 22•6 years ago
|
||
Comment 23•6 years ago
|
||
Comment 24•6 years ago
|
||
Comment 25•6 years ago
|
||
Comment 26•6 years ago
|
||
Comment 27•6 years ago
|
||
Comment 28•6 years ago
|
||
Comment 29•6 years ago
|
||
Comment 30•6 years ago
|
||
Comment 31•6 years ago
|
||
Comment 32•6 years ago
|
||
Comment 33•6 years ago
|
||
Comment 34•6 years ago
|
||
Comment 36•6 years ago
|
||
Comment 37•6 years ago
|
||
Comment 38•6 years ago
|
||
Comment 39•6 years ago
|
||
Comment 40•6 years ago
|
||
Comment 41•6 years ago
|
||
Comment 42•6 years ago
|
||
Comment 43•6 years ago
|
||
I talked to :nika today and came up with a possible solution involving "pausing" loadgroups during InternalLoad. It's going to take some testing and special casing for things like sync XHRs, but I'm hoping it's viable as a fix to this and other issues.
Comment 44•6 years ago
|
||
Suspend the docshell loadgroup while running the PermitUnload query
while loading a page. This will stop the permission dialog from being
dismissed.
Comment 45•6 years ago
|
||
Added a new patch for pausing the loadgroup while we query the user. This seems to both fix this problem and passes try:
https://treeherder.mozilla.org/#/jobs?repo=try&revision=ab37a1457a940537f2438ae3ab07fc3e65d9e356
I still need to integrate the load stalling test from this bug as an sjs test.
Updated•6 years ago
|
Updated•6 years ago
|
Reporter | ||
Comment 46•5 years ago
|
||
Hi! Do you know who to route this to so a new owner can be found? Thanks!
Comment 47•5 years ago
|
||
(In reply to Luan Herrera from comment #46)
Hi! Do you know who to route this to so a new owner can be found? Thanks!
I'm not best placed for this, unfortunately. Nika?
Comment 48•5 years ago
|
||
It looks like, looking at the spec, the correct behaviour here would be to cancel the existing fetch before we ever display the prompt to unload. That should theoretically prevent this issue from occurring, as we won't be able to cause the original load to finish while this load is ongoing.
I don't have a ton of extra time, but I'll try to find some time to look into this soon, and see how much doing that breaks.
Step 9 of https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate:
Cancel any preexisting but not yet mature attempt to navigate browsingContext, including canceling any instances of the fetch algorithm started by those attempts. If one of those attempts has already created and initialized a new Document object, abort that Document also. (Navigation attempts that have matured already have session history entries, and are therefore handled during the update the session history with the new page algorithm, later.)
Prompt to unload the active document of browsingContext. If the user refused to allow the document to be unloaded, then return.
Comment 49•5 years ago
|
||
(In reply to :Nika Layzell (ni? for response) from comment #48)
It looks like, looking at the spec, the correct behaviour here would be to cancel the existing fetch before we ever display the prompt to unload. That should theoretically prevent this issue from occurring, as we won't be able to cause the original load to finish while this load is ongoing.
I'm confused. IIRC the additional fetch is happening after the unload dialog has shown up (cf. comment #41). That is, AIUI the attempt (by the user) to navigate starts, and as part of step 10 in that attempt to navigate, we fire an event that triggers an image fetch (which doesn't itself navigate so wouldn't be cancelled by step 9 as quoted below, AIUI - and anyway, happens after step 9), and then while still in step 10 (because tab-modal prompts don't stop events/timeouts/... firing inside that docshell tree) the load of the image trips an event listener which starts a second attempt to navigate, while still in step 10 (because the dialog is up so the original attempt to navigate has not moved forward while all this happened). See comment #41 and comment #42.
So I don't think doing what you suggest would help with this bug... Am I missing something?
Step 9 of https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate:
Cancel any preexisting but not yet mature attempt to navigate browsingContext, including canceling any instances of the fetch algorithm started by those attempts. If one of those attempts has already created and initialized a new Document object, abort that Document also. (Navigation attempts that have matured already have session history entries, and are therefore handled during the update the session history with the new page algorithm, later.)
Prompt to unload the active document of browsingContext. If the user refused to allow the document to be unloaded, then return.
Comment 50•5 years ago
|
||
Nevermind, I misunderstood. Looking into other solutions.
Comment 51•5 years ago
|
||
Could we leverage whatever the JS debugger does when you're paused on a breakpoint to just stop scheduling of event/timeout/microtask runs in the given docshell tree while a tab-modal dialog is up? AIUI that would fix this...
Comment 52•5 years ago
|
||
(In reply to :Gijs (he/him) from comment #49)
I'm confused. IIRC the additional fetch is happening after the unload dialog has shown up (cf. comment #41). That is, AIUI the attempt (by the user) to navigate starts, and as part of step 10 in that attempt to navigate, we fire an event that triggers an image fetch (which doesn't itself navigate so wouldn't be cancelled by step 9 as quoted below, AIUI - and anyway, happens after step 9), and then while still in step 10 (because tab-modal prompts don't stop events/timeouts/... firing inside that docshell tree) the load of the image trips an event listener which starts a second attempt to navigate, while still in step 10 (because the dialog is up so the original attempt to navigate has not moved forward while all this happened). See comment #41 and comment #42.
Actually, I think the second attempt to navigate while the first attempt is still waiting for the prompt should be aborted by step 5:
- If the prompt to unload algorithm is being run for the active document of browsingContext, then return without affecting the prompt to unload algorithm.
A.K.A. the second load attempt would be ignored before even starting. I think the current checks for stuff like this (which appear to be occurring a bit late in our impl) are doing the opposite - allowing the unload if we're already within a permitUnload
block. https://searchfox.org/mozilla-central/rev/a777ff11b6d700a698c61e5bd17e73b044304494/layout/base/nsDocumentViewer.cpp#1269-1273
I might still be missing something though - I haven't spent as long looking at this stuff as others. (ni? :bz for potential insight)
Comment 53•5 years ago
|
||
(In reply to :Nika Layzell (ni? for response) from comment #52)
(In reply to :Gijs (he/him) from comment #49)
I'm confused. IIRC the additional fetch is happening after the unload dialog has shown up (cf. comment #41). That is, AIUI the attempt (by the user) to navigate starts, and as part of step 10 in that attempt to navigate, we fire an event that triggers an image fetch (which doesn't itself navigate so wouldn't be cancelled by step 9 as quoted below, AIUI - and anyway, happens after step 9), and then while still in step 10 (because tab-modal prompts don't stop events/timeouts/... firing inside that docshell tree) the load of the image trips an event listener which starts a second attempt to navigate, while still in step 10 (because the dialog is up so the original attempt to navigate has not moved forward while all this happened). See comment #41 and comment #42.
Actually, I think the second attempt to navigate while the first attempt is still waiting for the prompt should be aborted by step 5:
- If the prompt to unload algorithm is being run for the active document of browsingContext, then return without affecting the prompt to unload algorithm.
A.K.A. the second load attempt would be ignored before even starting. I think the current checks for stuff like this (which appear to be occurring a bit late in our impl) are doing the opposite - allowing the unload if we're already within a
permitUnload
block. https://searchfox.org/mozilla-central/rev/a777ff11b6d700a698c61e5bd17e73b044304494/layout/base/nsDocumentViewer.cpp#1269-1273I might still be missing something though - I haven't spent as long looking at this stuff as others. (ni? :bz for potential insight)
FWIW, this sounds right to me. The reason we allow the unload atm is to make it so that users can close tabs (or navigate them) when the dialog is up, ie when the dialog is simply "in the way", attempting to close / navigate the tab the second time should Just Work.
We might be able to keep that if we inspect the triggering principal of the request to navigate, or we might be able to come up with a different way to make the "click the close button twice" usability thing work irrespective of how docshell behaves.
Comment 54•5 years ago
|
||
allowing the unload if we're already within a permitUnload block
That check is for whether the beforeunload stuff should run. It's a reentry prevention.
The "prevent navigation during beforeunload" bit is at https://searchfox.org/mozilla-central/rev/e18057a9613ffda06dfd3640209ca234ed7dc37d/docshell/base/nsDocShell.cpp#3716-3717 and makes nsDocShell::IsNavigationAllowed
return false if beforeunload is firing.
So I just took a look at the spec, and now I'm confused. Either the spec changed or I misread it in comment 41. Probably the former, because the step numbers are all different.
At this point the spec says to cancel the old navigation in step 9 and fire beforeunload in step 10. See https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate. Then if the beforeunload allows the unload all other fetches are canceled in step 11. If we implemented this spec as written, it would solve this bug, right?
Comment 55•5 years ago
•
|
||
So it looks like the spec has said to do that for a while, so I misread it and it has changed. I am pretty sure we do not implement that as written, and it sounds like we should!
Comment 56•5 years ago
|
||
I haven't had the time to get around to this over the last few months, as I've been quite busy with other work, but I took a quick look at the logic today.
Currently, if an existing beforeunload
is being handled for the active document, it returns a success value here. This logic appears to have been added in bug 864247. The obvious fix from the point-of-view of the standard would be to change this behaviour to instead reject the unload attempt.
As mentioned in comment 53, this may not be desirable for loads triggered by the UI, as it means that a beforeunload handler could prevent the user from navigating a tab using the browser UI. To handle that, we'd need to pass a triggering principal into nsIContentViewer::PermitUnload
and check if it is the system principal. However, if we allow the nested load, we need to somehow prevent the outer load from occurring. I think we'll need some sort of mAllowedNestedUnload
flag, which needs to be checked upon exiting from the permitunload
event handler or the dialog, and automatically reject unloading if set. This should ensure that only one load is allowed to proceed past beforeunload at a time.
Even with those changes, though, I'm a bit worried about re-entrant loads due to subframes. After beforeunload
is fired for the toplevel document, it is recursively fired in all subframes. However, neither mInPermitUnload
nor mInPermitUnloadPrompt
appear to be set on the parent nsDocumentViewer object while these nested events are firing. I worry something ~like this might be an issue:
- Navigation 1 is attempted, and
beforeunload
fired in subframe. Subframe is removed from document, and nested event loop is created using sync-xhr. - Navigation 2 is started. No flag is set on the toplevel document, and subframe is no longer in tree, so the navigation begins.
- sync-xhr is resolved, and navigation 1's
beforeunload
is permitted to continue, leading to multiple concurrent navigations on the same docshell.
We may need to have a flag for the nsDocumentViewer which is set for the entire duration of PermitUnloadInternal
, and check the mAllowedNestedUnload
flag after firing the event on subframes as well.
Comment 57•5 years ago
|
||
Boris, could you take this since Nika is on leave?
Comment 58•5 years ago
|
||
Is this a high priority? This is a lot of investigatory and spec work to figure out what the behavior should be and define it, and I don't know that I can squeeze that in along with the other things I need to do right now.
Comment 59•4 years ago
|
||
Unfortunately this bug doesn't meet our criteria for spoofing bounties, both the user interaction required (narrow scope of attack) and lack of a lock icon indicating you've securely loaded the fake page.
Updated•4 years ago
|
Updated•4 years ago
|
Updated•4 years ago
|
Comment 61•4 years ago
|
||
NI for trying to repro it with the latest Fission architecture.
Comment 62•3 years ago
|
||
I'm not finding the time to get to this, so going to transfer the NI to Nika to try and see if this still is repros.
Updated•2 years ago
|
Updated•2 years ago
|
Updated•6 months ago
|
Description
•