Firefox 3.6/PushState Security Review
Implement the HTML5 push/pop/replace state API.
Security and Privacy
- Sites can change their displayed URL with pushstate, but they shouldn't be able to push to a new domain. HTML5 specifies standard same-origin checks here; these are already in the code.
- If site A calls pushstate with data object O, site B (of different origin than A) shouldn't be able to access O. This is built into the design, since the popstate event only gives a page back a state object set by another page from the same origin.
- A malicious site might try to flood the session history with many history entries. The spec allows us to place limits on the number of entries a page may create:
User agents may limit the number of state objects added to the session history per page. If a page hits the UA-defined limit, user agents must remove the entry immediately after the first entry for that Document object in the session history after having added the new entry. (Thus the state history acts as a FIFO buffer for eviction, but as a LIFO buffer for navigation.)
Although a malicious site could spam the session history today by, for example, changing the page's hash, pushstate may be a more attractive attack vector because the page can remain completely responsive while the session history entries are being added. As a result, we may want to aggressively limit the number of states a page can push. Here's the spec's suggestion:
Security: It is suggested that to avoid letting a page "hijack" the history navigation facilities of a UA by abusing pushState(), the UA provide the user with a way to jump back to the previous page (rather than just going back to the previous state). For example, the back button could have a drop down showing just the pages in the session history, and not showing any of the states. Similarly, an aural browser could have two "back" commands, one that goes back to the previous state, and one that jumps straight back to the previous page.
- Instead of / in addition to hard per-page limits on the number of remembered states, we may need to rate-limit pushstates. Otherwise, a malicious site could keep a user from going back by pushstating every 10ms to the same site. Its oldest history entries would constantly be dropped, but it would still own the history. We'd have to take care to ensure that the rate limiting doesn't appear to be arbitrary.
- We also have to try and prevent a site from DOSing the browser by pushing many large objects.
- If we save pushStated data to disk (say for crash restore purposes), then we're allowing webpages to write arbitrarily large data do disk. We should probably put a limit on this. Additionally, if we allow serialization of binary-like objects (images?), we may be allowing pages to write truly arbitrary, potentially executable data to disk.
- What security check should we do on the new URI after a pushstate? Do we want to support pushstate for chrome:// and file:// ?
- Should a page at http://foo.com be able to push to http://user:email@example.com? HTML5 says no, but CheckSameOriginURI says yes.
- History.pushState(aState, aTitle, [aURL])
- History.replaceState(aState, aTitle, [aURL])
- PopState event
We use the JS library's native JSON parsing to encode/decode objects. Worker threads use the same library for a similar purpose.
- eviction from bfcache
doing stuff during popstate event?
- history.back() -- might cause another popstate
- setting location
- setting hash
- close the window
Extensions that want to keep track of your navigation
- What data is read or parsed by this feature? We don't parse any data, although we do generate JSON data from supplied JS objects and deserialize it when firing PopState. We also write this JSON to disk for session restore.
- What is the output of this feature? New session history entries, PopState events.
We can turn push, pop, replace state off with about:config prefs.
Relationships to other projects
- Smaug is planning to / in the process of rewriting session history. I'm not sure exactly what that entails, so I'm not sure how it affects me. It's probably not a security issue, though.
- clearState is not going to be implemented because use cases are not clear, saving that for later when the use cases show up. If this gets added, it should be reviewed for security issues.
- Disallow user:pass in pushstate'd URIs.
- if the current document was opened with userinfo in its URI ignore that for purposes of comparing against pushState's URL.
- Push/replace states to same URI shouldn't count for global history.
- we should flag pushState() entries so the awesome bar frequency analysis could count these differently if they need to in order to prevent abuse
- Maybe limit the size of the data that can be passed to pushState()? Some amount of it gets saved to session restore (need to check, might be only a certain number of history items--like 10?--saved per window)
- 500K seems generous enough for anything. How would it be measured? Probably after serialization to JSON.
- maybe we need a URI length-limit in general? Would limit the things you can do with a data: uri though.
- When we pushState() to a new URL the browser chrome gets a location change notification, but the page itself does _not_ get any events. Not even the normal "hashchange" events (if you change just the hash). A page using pushState() will have to send its own events if it needs them.
- view-source and reload will both fetch the content at the new URI if one was pushed.
- if you pushState(,,<url>) on a page that's the result of a POST, navigate away and then go back, do you use POST or GET on the new url?
- file:// uris have odd origin rules. We certainly don't want to allow a file to pushState() to some URL it can't otherwise load, navigate to a cooperating server which goes back to the inaccessible file. Maybe our current same-origin checks embody the correct rules.
- jst and dveditz wouldn't mind not supporting the <url> parameter at all for file: uris, sicking thinks the current same-origin rules do the right thing.
- popState should not count as a user interaction for allowing popups.