Skip to content

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

Closed

Description

@SpyroTEQ

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.

Activity

  1. zcorpan commented on Nov 8, 2016

    @zcorpan
    Member

    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.

  2. SpyroTEQ commented on Nov 8, 2016

    @SpyroTEQ
    Author

    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.

  3. annevk commented on May 4, 2017

    @annevk
    Member

    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.)

  4. jakearchibald commented on Aug 11, 2020

    @jakearchibald
    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.

  5. emilio commented on Aug 11, 2020

    @emilio
    Contributor
  6. jakearchibald commented on Aug 11, 2020

    @jakearchibald
    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.

  7. smaug---- commented on Aug 11, 2020

    @smaug----

    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.

  8. jakearchibald commented on Aug 11, 2020

    @jakearchibald
    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.

  9. jakearchibald commented on Aug 11, 2020

    @jakearchibald
    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.

  10. domenic commented on Mar 16, 2022

    @domenic
    Member

    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.

  11. domenic commented on Mar 16, 2022

    @domenic
    Member

    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).

  12. camillelamy commented on Mar 18, 2022

    @camillelamy
    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.

  13. domenic commented on Mar 31, 2022

    @domenic
    Member

    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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions