Repository navigation
Cancelling UI initiated navigations (back/forward) #32
Description
Activity
To prevent someone from trapping a user on their page by canceling all navigations.
To prevent someone from trapping a user on their page by canceling all navigations.
I updated the description, what I said is only relevant for the same origin and document (e.g. SPA) navigations
Hey, thanks for opening this! Unfortunately, we can't allow intercepting the back/forward buttons, even for same-document navigations. Consider the following attack:
- The user visits your site
- You call
appHistory.pushNewState()the first time they click a button, performing a same-document navigation. - You now intercept the
navigateevent and always calle.preventDefault()ore.respondWith().
You have now completely disabled the user's back button, since pressing back does a same-document navigation, but that navigation never succeeds.
This isn't an acceptable outcome for our users, so we can't allow interception in this way.
We can allow interception of same-document URL-bar triggered navigations, i.e. if the user changes the fragment component. I'm working to clarify that in #26. There's also no problem with intercepting history.back(), or clicking links; those are all interceptable. But for the browser UI's back/forward button, interception is too abuse-prone, even same-document.
Does this make sense?
We can allow interception of same-document URL-bar triggered navigations, i.e. if the user changes the fragment component.
Can we intercept it if they change the pathname or the search through the back and forward buttons?
Does this make sense?
To me, this part isn't clear 😅 :
This isn't an acceptable outcome for our users, so we can't allow interception in this way.
As a developer, we can create pop-ups and move them around. We can make the experience as annoying as possible but nobody would use our website if we wanted to. To me, that has always been part of software.
I understand browsers want to protect their users to some extent and I think being able to lock the user while they are on the same web application is where to draw the line because it enables enhanced user experiences like avoiding losing changes by accident (a two finger swipe, trying to resize/show a sidebar for example). Users are never trapped as they can close the window/tab or change the URL and the application won't be able to intercept that. Or at least, that's what I would like to have.
FWIW, if I cannot intercept same document/URL navigations from anywhere, I won't be able to benefit from e.preventDefault() or e.respondWith() as much as I wished because I will have to make the code work in all scenarios and therefore fall back to what I'm doing right now by saving the position in the history state to compute a delta and restore the navigation (which can still be escaped by the user by pressing multiple times the back button).
It's still great for all the other improvements it brings but I fail to see how intercepting same domain navigations no matter where they come from are abuse-prone.
I also don't understand why history.back() is interceptable but clicking the back button isn't. Don't they trigger the same behavior? 🤔
Can we intercept it if they change the pathname or the search through the back and forward buttons?
No; the back and forward buttons are not interceptable, for the reasons explained above and below.
I understand browsers want to protect their users to some extent and I think being able to lock the user while they are on the same web application
The problem is that it crosses the boundary. The user is trying to exit your web application, using the back/forward buttons. And you're not letting them. That's what's not acceptable. This is true even if the current navigation is same-document, because of the example I gave above.
I understand you think that the close button and location bar editing are an acceptable alternative, but our believe is that this isn't good enough---especially on mobile.
I fail to see how intercepting same domain navigations no matter where they come from are abuse-prone.
Hmm, I tried to make this pretty clear in my above example. Is the issue that you don't believe disabling the user's back button is abusive? Maybe that's the root of our disagreement.
I also don't understand why history.back() is interceptable but clicking the back button isn't. Don't they trigger the same behavior? 🤔
They do, but this isn't about the effect; it's about which party is trying to go back. One is user-triggered, and the other is web-developer triggered. There's no problem with the web developer overriding their own attempt at navigation. The problem is when the web developer's desires to prevent back interferes with the user's attempt at navigation.
I do want to emphasize we're not 100% confident in our thinking. I've been making categorical statements which might imply that I insist on the current restrictions, but what I've been trying to be categorical about is that we can't allow abuse and trapping the user. There might be other ways of accomplishing this than the current proposal.
For example, you could imagine well-specified rules like "the second back button push will go through if the first one was intercepted" which would allow the user to escape a site with only a bit more friction. Or, the browser could pop up a dialog saying "It seems this site might be trying to trap you. Would you like to kill it?" if you try to intercept a back button navigation. (Maybe only after you do it a couple of times.)
One way to approach such rules is figuring out how browsers handle abuse abusive back-button-disabling experiences today. Although I can't guarantee that if we found an abusive experience we'd be OK with letting appHistory replicate it---more likely we'd look at fixing it in the existing APIs---it'd be at least an interesting starting point. For example, navigating to
- https://example.com/ using your URL bar
- https://output.jsbin.com/xaniyazowe/2 in the same window using your URL bar
and then pressing back, results in you being trapped in Firefox, but able to escape in Chrome. Chrome seems to prevent the trap by having the back button skip the intermediate history entry. Maybe that's a route forward here, where we could allow navigation interception, but skip (some? which?) intercepted navigations whenever the user uses the back button? Hmm.
Hmm, I tried to make this pretty clear in my above example. Is the issue that you don't believe disabling the user's back button is abusive? Maybe that's the root of our disagreement.
This is the part that isn't clear for me and I do think it's the root of me not being able to properly explain the valid use case we see today in applications and is a bit hackish so let me rephrase. In my opinion:
- The user should be able to leave the web application (e.g. going from online-word.com to example.com) by using the browser UI back/forward buttons and the developer should not be able to intercept these navigations
- The application should be able to prevent the user from accidentally leaving the current page to go to a different page in the application (e.g. going from online-word.com/documents/20 to online-word.com/documents no matter what initiated the navigation because they would be losing unsaved changes)
Currently, when pressing back or forward, the URL immediately changes. For client-side routers, this means that any navigation guard that would prevent the navigation, will have to restore the previous entry with history.go(-delta) (delta being a number different from 0 and representing the number of entries the user traversed) if the navigation has to be cancelled, e.g. in a navigation guard.
and then pressing back, results in you being trapped in Firefox, but able to escape in Chrome. Chrome seems to prevent the trap by having the back button skip the intermediate history entry
I have the same result on OSX on both browsers: being able to leave
One possibility is to separate the entry position displayed by the UI buttons from the URL being displayed and the information being passed to the application. Given the history: [example.com, word.com/documents, word.com/documents/20] with unsaved changes, back button is enabled, forward is disabled,
- The user clicks back once, the URL stays word.com/documents/20, the web application displays a message and prevents the navigation but the UI buttons display as if the navigation happened: the forward button is enabled
- The user clicks back again, the web application cannot intercept the navigation because it's not the same domain, the browser goes back to example.com
It's up to the router implementation to properly handle unfinished navigations (e.g. if there are more than 2 entries with the same domain URL and the user goes through them) no matter where they started from.
I think there's definitely trade-offs to be made here, and we could allow this if we also had creative solutions to address abuse. I tend to side with "trusting the application" not to do the wrong thing, but I also think that we do need to give users the power to ban abusive websites from using controls if they have shown they're abusive.
E.g., we have the ability to say "Stop this page from showing me alerts". Maybe we also need to have a "Stop this page from writing to history".
A user could turn off JS for a page at any point. Not many users are familiar enough with how web technologies work to know that usually abusive history patterns derive from JS usage of history APIs.
I was just thinking about the back button and wondering if it would count as a userInitiated navigation, and was also surprised that it wouldn't even trigger a navigate event.
I don't understand the use cases for "navigation guards" when navigating backwards, and I certainly share @domenic 's concerns about trapping.
However, I'd like to know, functionally, how is back expected to work for SPA if it doesn't fire a navigate event? Will it force a hard navigation? That feels like a bad result.
(Also, isn't back button already interceptable and widely used via onpopstate?)
However, I'd like to know, functionally, how is back expected to work for SPA if it doesn't fire a navigate event? Will it force a hard navigation? That feels like a bad result.
The same way as it does today. If the history entry being navigated back to is same-document, then it'll do a same-document navigation. That will fire all the appropriate after-the-fact events, such as currententrychange, which can be used to observe the process.
(Also, isn't back button already interceptable and widely used via onpopstate?)
The popstate event does not allow intercepting the back button. It allows observing a back navigation after the fact. That's the role that currententrychange serves in app history.
I don't understand the use cases for "navigation guards" when navigating backwards, and I certainly share @domenic 's concerns about trapping.
Bottom of #32 (comment), tldr: leaving a page by accident and losing changes. In reality, it's the same as a regular navigation guard, it's just that the navigation happened using the back button instead of clicking on a link, but it's still a navigation from the router perspective
FYI, I'm working on a pull request to solve #53 by allowing respondWith() for such navigations, even if we don't allow preventDefault() yet.
On allowing preventDefault(): similar concerns about leaving a page by accident and losing changes came up in an offline discussion with @mjackson and @ryanflorence. It seems like this is an important use case we need to solve.
A tentative proposal the three of us came up with is that you get one "free" preventDefault() after the user has interacted with your page. If you want a second one, the user must interact with your page again between the two preventDefault() calls. For example, if the user clicked on a "No, let me save my work" button, that would reset the preventDefault() guard, and let you cancel it again.
Note that this doesn't solve the "route guard" case of preventing access to logged-in state after you're logged out. However, maybe that is better solved by an API like whatwg/html#5744 (and perhaps an appHistory counterpart, which is in the same spirit as #9).
The other route we could go to solve the unsaved data case is to come up with something more specifically targeted at that for same-document navigations, like beforeunload is targeted at that case for cross-document navigations today. That is, some API where the browser explicitly pops up a dialog box asking about whether you're sure you want to leave or not. This has pros and cons versus having pages manage such a dialog themselves while using preventDefault().
14 remaining items
I'm confused because I understood there was an agreement on being able to cancel user-initiated same-document traversals to prevent the user from losing data when leaving a page.
There is agreement on doing so in the future, when we can do the extra implementation work (requires lots of extra complexity since it causes cross-process messaging) and anti-abuse work (what is mentioned in what you quote).
They still trigger a navigate event, don't they? that way, we can still rollback to the previous history entry if necessary even if the URL changes.
Correct.
It's cristal clear now, thanks!
Is preventing preventDefault on browser back button invocation enough?
Could a threat actor use the navigation API in it's current form to change window.location, open a new tab or trigger a blocking interaction like an alert, in response to a navigation event?
I would like to see the navigation event fired even if user used back/forward functionality. I have already had to circumvent around unload events not firing if not enough interaction has happened (unload events were used for closing opened windows from SPA).
I support allowing cancelling UI initiated navigations once but if not then the next best would be that event would be fired but you couldn't prevent navigation (preventDefault and intercept blocked).
Shouldn't alert be blocked in same way as is done in unload events? If we also block location change then threat actor can only open a new tab/window but it can be done only once as the user will close the opened window (unload events won't fire).
I would like to see the navigation event fired even if user used back/forward functionality. I have already had to circumvent around unload events not firing if not enough interaction has happened (unload events were used for closing opened windows from SPA).
I support allowing cancelling UI initiated navigations once but if not then the next best would be that event would be fired but you couldn't prevent navigation (preventDefault and intercept blocked).
Have you tried the navigation API? This is already the behavior.
I replied based on the discusions here but yes it seems to behave that way (although there seems to be some problems with debugging at least back navigation event, which I have reported to chrome moments ago).
Hi all, I'm here to give an update on our progress on this issue. (Really, it's all @natechapin's progress!)
We've updated the explainer in a430943 and the spec in 7ece8d7 to allow canceling traversals in some cases. Specifically, we currently allow such cancelation:
- Only in the top window, not in any subframes
- Only if either:
- The traversal is programmatic, e.g. via
navigation.back(); or - The traversal happens when there is user activation, and the user activation is consumed. E.g., if the user types something, and then quickly (within the transient activation duration) presses the browser back button.
- The traversal is programmatic, e.g. via
Simultaneously, Nate has worked on a behind-a-flag change to Chromium to prototype this, which is undergoing review. As mentioned previously, this is a pretty tricky change from an implementation perspective, so that review will take some time. But we're optimistic.
However, the above limitations are too strict, and are not what we want to ship. Instead, we want to loosen "The traversal happens when there is user activation" to something more like "There has been non-consumed user activation at some point in the past". This avoids the short time limit, and makes it so that:
- If you type something, walk away for 10 minutes, and then come back and press the back button, that navigate event is cancelable
- If you don't interact with the page at all, then that navigate event is not cancelable
We're calling this concept "consumable sticky activation", since it's a variant of sticky activation. It'll be a generalization of other work we've previously done for the not-yet-shipped CloseWatcher proposal.
The exact details are still under some discussion, so that'll add a bit more time. But I wanted to let people know there's been significant progress here, and that it's our current main work item for improving the navigation API now that v1 has shipped!
That’s awesome — it sounds like the consumable sticky activation model would capture what’s needed precisely. Way better than [insert implementation-specific heuristics here]!
I spun out a separate issue to discuss a particular aspect of this API, which is how exactly to allow it to block some traversals without slowing down all traversals: #254. Thoughts appreciated, especially on the question of whether blocking cross-document traversals is important.
Hey folks! Cancelable same-document traversals are now available in Chromium ≥112.0.5613.0, including the appropriate safeguards against back-trapping. Please try them out in the demo at https://gigantic-honored-octagon.glitch.me/ ! (There's a new checkbox at the bottom to try to cancel the traversals.)
From the issue-tracking point of view, I think the explainer is still pretty accurate, except for not being updated for the conclusion of #254. The spec updates are being incorporated into whatwg/html#8502 . And web platform tests have fully landed. So we can probably close this out!
It sounds like you are describing some sort of Chromium bug. A closed issue on a web specification is not the best place to report those; I suggest https://crbug.com. And as always with bug reports, minimal reproduction cases are key.
Hello, looking at the part regarding navigation cancellation (https://github.com/WICG/app-history#navigation-monitoring-and-interception) and later on, the comparison with navigation guards makes me wonder why can't UI based navigations (or
history.back())be cancelled as well withevent.preventDefault()for same document and origin navigations? If they can't be cancelled navigation guards cannot be implemented like proposed since they also run when the user navigates backwards and forwards.Currently, on Vue Router 4, I restore the history entry (when possible): https://github.com/vuejs/router/blob/main/packages/router/src/router.ts#L1039-L1042 when a ui initiated navigation is cancelled.
It would be even nicer if the URL stays the same until the navigation is confirmed, no matter what initiated it, making it consistent when doing
history.back()(or clicking the ui button) or clicking on a link of the application. In applications, users show modals or confirmations messages when before navigating away from a page (e.g. to not lose unsaved changes) and the URL changing is often confusingAll of the above is for same origin and document navigations, which is the context of an SPA.