Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should History.length really be cross-domain [XSHM breach]? #2018

Closed
SpyroTEQ opened this issue Nov 8, 2016 · 13 comments · Fixed by #9157
Closed

Should History.length really be cross-domain [XSHM breach]? #2018

SpyroTEQ opened this issue Nov 8, 2016 · 13 comments · Fixed by #9157
Labels
security/privacy There are security or privacy implications topic: history

Comments

@SpyroTEQ
Copy link

SpyroTEQ commented Nov 8, 2016

Current status of History length

As stated in https://html.spec.whatwg.org/multipage/browsers.html#dom-history-length:

The length attribute of the History interface, on getting, must return the number of entries in the top-level browsing context's joint session history.

This means that the length attribute of the History interface is cross-domain.

Consequence: Cross Site History Manipulation

Generic attack scenario

A webpage can guess the browing history of a nested browing context by keeping a trace of the History length value. As an example, a document can store the value of History length, then open a nested browsing context to another domain, and track the browing actions in that nested browsing context by smartly testing when History length changed.

Online example

An attacker document (called [AD], like https://tests.reinom.com/http/xshm/ ) opens a nested browsing context [NBC] and loads a redirection's target (called [RT], https://dom2.tests.reinom.com/http/xshm/ ) in that [NBC]. Then, [AD] stores the History length value, changes [NBC] URL to a vulnerable page [VP] that does a conditionnal redirection. Once the [NBC] is loaded, the [AD] checks whether History length has changed. If it has not, it means that the [VP] redirected to [RT]. If it has, it means [VP] redirected to another page than [RT] (or did no redirection at all). So [AD] knows whether the redirection from [VP] to [RT] occured (so they knows if the condition of that redirection was true).

Suggested solution

If History length violates the SOP principle, then it can be used to guess informations about a nested browsing context navigation that does not share the same domain-origin. I suggest that the History length attribute should be filtered before being accessed by a Document and only return entries that matches its domain.

Either like the short:

The length attribute of the History interface, on getting, must return the number of entries in the top-level browsing context's joint session history. These entries must be first filtered to keep only the ones sharing the same origin-domain that this Document associated to the History object.

Or the longer but closer to "joint session history" definition ("with all entries ... removed"):

The length attribute of the History interface, on getting, must return the number of entries in the top-level browsing context's joint session history, with all entries that do not share the same domain-origin than the Document associated to the History object removed.

With a link to https://html.spec.whatwg.org/multipage/browsers.html#same-origin-domain on "same origin-domain".

Impact

IMO, the impact this may have is very minor. It will only break reliance on cross domain History, like knowing whether the Document was directly loaded in a browsing context or if it was loaded after clicking a link from another domain. This can still be known with HTTP's Referer header.
But it will break XSHM attacks and enhance privacy security for Web users.

@zcorpan zcorpan added security/privacy There are security or privacy implications topic: history labels Nov 8, 2016
@zcorpan
Copy link
Member

zcorpan commented Nov 8, 2016

This is the same issue as https://www.owasp.org/index.php/Cross_Site_History_Manipulation_(XSHM) correct? Has any browser taken any steps to mitigate the attack?

Checking the demo https://tests.reinom.com/http/xshm/ in Chrome, the malicious page thinks the cookie is set both when it is actually set and when it isn't.

@SpyroTEQ
Copy link
Author

SpyroTEQ commented Nov 8, 2016

Yes, it's the same issue.
Firefox team member Benjamin Smedberg said that this is a spec issue ( https://bugzilla.mozilla.org/show_bug.cgi?id=1315203#c2 ) so Mozilla won't mitigate it if the spec remain unchanged.

It appears the XSHM occurs in Firefox (47) and IE (11). In Chrome (48), Opera (41), it doesn't. So it may be related to the way History entries are added when a 3xx redirect occurs. I'm not very sure about what the specs says in such situation, so I cannot tell which of (Firefox+IE) or (Opera+Chrome) is wrong about the spec's implementation.

@annevk
Copy link
Member

annevk commented May 4, 2017

This is blocked on #1454.

Would be interested to hear what @mikewest and @johnwilander think about this one. (Apologies for all the recent pings. I've been looking at older unsolved issues.)

@jakearchibald
Copy link
Contributor

Not only is this a security/privacy issue, it's also really difficult to manage in a world where nested navigables can be in different processes.

history.length changes synchronously in the following cases:

  • On hash change or pushState. This involves decementing history length by the number of forward session history items, and adding 1. Onward session history items may be spread across a series of processes, so the current thread needs some copy of this number, which means it can get out of sync.
  • On iframe removal. This involves decementing history length by the number of session history items (both forward and backward) owned by the iframe's navigable and its descendant navigables, deducting one for each navigable so initial history items aren't counted. Again, these can be spread across a series of processes.

Chrome is currently the only browser that ships other-process iframes, and it's pretty easy to end up with history.length being out of sync, and we don't currently discard session history items when iframes are removed.

I asked folks what they used history.length for, and it seems like the use-cases are pretty unreliable without an accompanying history.index, which would just make the implementation & security problems worse.

Some thoughts on ways forward:


history.length always returns 42.

This solves the implementation complexity/races, and the security issues. It will break script that relies on history.length changing.


history.length updates async.

This solves the implementation races. It doesn't solve the security issues. It will break script that relies on history.length changing synchronously.


history.length is updated synchronously, but it's per tuple [origin, event loop, browsing session].

history.length will start at 1, and the following will modify that value synchronously:

  • pushState or changing the hash.
  • Navigating to another page on the same origin, and the same event loop is used (as in, it doesn't need to isolate due to COOP/COEP)
  • Navigating same-origin & event-loop iframes.
  • Removing same-origin & event-loop iframes.

The following will not modify that value:

  • Navigating to another origin/event-loop page. This will use another history.length associated with that [origin, event loop, browsing session].
  • Navigating/removing iframes on a different origin or loop. Again, they will have their own history.length.

This may act inconsistently in browsers that create new event loops for pages that are in the same origin & browsing session. But I'm not sure that's any more inconsistent than history.length currently is across browsers.

I think this solves the implementation complexity/races, and the security issues. It isn't clear to me if this would still break scripts that rely on the current behaviour of history.length. It might even be more reliable for those scripts?


Any takers for the above? I'm not trying to "fix" the history API, I'm just trying to resolve the security and race conditions around history.length without breaking too much of the web.

@emilio
Copy link
Contributor

emilio commented Aug 11, 2020

cc @mystor @smaug----

@jakearchibald
Copy link
Contributor

If folks are using history.length to answer "can I go back" then I guess we can make it 1 or 2 when navigating to a new [origin, event loop, browsing session] depending on whether there's a previous entry at the time.

@smaug----
Copy link

It would need to be 3 to answer to the question "can I go back and can I go forward". And also if pushState or fragment navigation is used, it would be odd if .length didn't increase.

@jakearchibald
Copy link
Contributor

Actually, I'm talking shit, just because history.length is 2 doesn't answer "can I go back", as you'd need the current index too.

@jakearchibald
Copy link
Contributor

And also if pushState or fragment navigation is used, it would be odd if .length didn't increase.

Right, but history.length is already odd in implementations. But yes, I agree that might be too odd.

@domenic
Copy link
Member

domenic commented Mar 16, 2022

A webpage can guess the browing history of a nested browing context by keeping a trace of the History length value. As an example, a document can store the value of History length, then open a nested browsing context to another domain, and track the browing actions in that nested browsing context by smartly testing when History length changed.

This generic attack is worse than just history.length. It is inherent in the joint session history design plus anything that lets you tell whether a history.back()/etc. has traversed the joint session history.

For example, here is a version that uses history.state updating: http://boom-bath.glitch.me/session-history-leak.html . (Note that it is cross-origin to the https: nested iframe. You can also test with the same code in jsbin for a jsbin-spying-on-glitch example that is cross-site: https://output.jsbin.com/wepicaqewo .)

You could do a similar thing without history.state by using URL fragments: http://boom-bath.glitch.me/session-history-leak-hash.html . (Or using URL paths with history.pushState()/history.replaceState().)

Both versions require potentially navigating the child frame which might be a bit destructive for an attacker, but it's possible there are clever ways around that.

Of course this attack is also possible using iframe load events, without any history stuff involved!

In conclusion, telling when nested browsing contexts navigate is not really hard on the current platform, so I am not sure how concerned we should be.

@domenic
Copy link
Member

domenic commented Mar 16, 2022

I guess maybe I misunderstood the original attack. It's not the fact that the page navigated that we consider sensitive. It's the URL the page navigated to.

That can be mitigated, as Chromium has done, by avoiding the same-URL-replace behavior when the initiator is cross-origin: https://bugs.chromium.org/p/chromium/issues/detail?id=1208614 (although I don't think that bug is public).

@camillelamy
Copy link
Member

I am wondering if there is additional information being revealed by the joint history state compared to listening to onload events from the iframes, beyond the potential URL issue you point to. For example, if a cross-origin iframe were to navigate in a manner that causes the current history entry to be replaced rather than a new one being added, this is something that could be seen through the evolution of the joint history but not by listening to the iframe load events, which I think would be similar in both cases.

Additionally, the history API is accessible from iframes I believe, which makes it possible for an iframe to leak data from siblings iframes right? Which wouldn't be possible from just listening to onload callbacks.

So overall, it would seem to me that the history API has XSite leak capabilities that go beyond listening to load callbacks. Since it also seems that it is not the most handy API anyway, it would be good if the XSite leak capabilities were mitigated.

@domenic
Copy link
Member

domenic commented Mar 31, 2022

For example, if a cross-origin iframe were to navigate in a manner that causes the current history entry to be replaced rather than a new one being added, this is something that could be seen through the evolution of the joint history but not by listening to the iframe load events, which I think would be similar in both cases.

Sort of. It would need to be joined with other information, such as load events, to be distinguishable from the cross-origin iframe doing nothing at all.

Additionally, the history API is accessible from iframes I believe, which makes it possible for an iframe to leak data from siblings iframes right? Which wouldn't be possible from just listening to onload callbacks.

Well, you cannot access parent.frames[0].history. But, window.history (which you can access) gives indirect information about sibling iframes, of the type we're discussing. So yeah, you can tell whether a sibling iframe navigated.

So overall, it would seem to me that the history API has XSite leak capabilities that go beyond listening to load callbacks. Since it also seems that it is not the most handy API anyway, it would be good if the XSite leak capabilities were mitigated.

Do you have any ideas how to do that? If our goal is not just to protect the URL, but instead to protect any information leakage beyond load events, I think the type of attacks I mentioned in #2018 (comment) where you use history.back() or history.go(N) to manipulate the iframe, and then read back the impact on yourself, make this pretty hard to mitigate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
security/privacy There are security or privacy implications topic: history
Development

Successfully merging a pull request may close this issue.

8 participants