DOM vs. Web
Thursday, 20 April 2006
Back at the W3C Technical Plenary, I argued that Working Groups need to concentrate on making more Web-friendly specifications. Here’s an example of one such lapse causing security problems on today’s Web.
Safety in HTTP
HTTP methods have a property known as safety; RFC2616 describes it in section 9.1.1;
Implementors should be aware that the software represents the user in their interactions over the Internet, and should be careful to allow the user to be aware of any actions they might take which may have an unexpected significance to themselves or others.
In particular, the convention has been established that the GET and HEAD methods SHOULD NOT have the significance of taking an action other than retrieval. These methods ought to be considered “safe”. This allows user agents to represent other methods, such as POST, PUT and DELETE, in a special way, so that the user is made aware of the fact that a possibly unsafe action is being requested.
This philosophy can be seen throughout HTTP; for example, if you redirect an unsafe method (say, POST), the client has to get permission from the user before following it automatically.
Safety isn’t just a nice idea; it’s one of the foundations of the Web, making things like search spiders, caches and GET as the default method for URIs possible. Probably the most widely-known example of the consequences of messing with safety in HTTP is the pain seen by Web sites that did unsafe things with GET once Google Web Accelerator came around.
HTML (and other Web formats) also take advantage of safety; tags like IMG, IFRAME, and SCRIPT only use GET. That way, a page can’t trick your browser into doing something unsafe without you knowing about it. In fact, the only way to get a POST out of a browser is with an HTML form. Almost.
The form.submit() attack
Unfortunately, that design didn’t carry through into the DOM Level 2 HTML spec, which included the HTMLFormElement.submit() method without any security considerations. Once bound to JavaScript as form.submit(), this allows any site to POST any form to any other site.
For example, imagine if this was included in a random Web page;
<html>
<body>
<p>Nothing to see here...</p>
<form action="http://some.other.site/your/bankaccount" name="f">
<input type="hidden" name="amount" value="$1,000,000">
<input type="hidden" name="destination" value="mnot's secret bank account">
</form>
<script type="text/javascript">f.submit();</script>
</body>
</html>
Since the request will be sent with any cookies or HTTP authentication you have for some.other.site, if you’re logged into your bank account, I get your money (presuming the form is well-crafted enough).
This hole is real, and present in all browsers today. Web sites that want to protect against this have to take special measures to assure that the request actually came from the user, rather than from another site.
Oops.
“No New Vulnerabilities”
This is by no means a new problem; it’s been widely known for some time [pdf]. However, instead of actually trying to fix it and preserve the benefits of safety, the attitude now seems to be that we should use it as an excuse to intentionally standardise a bunch of other holes, because there are “no new vulnerabilities”.
For example, a proposal to allow XmlHttpRequest to send a POST to other sites specifies that there should be an “access check” to determine if the contents of the response should be made available to the browser. This will catch some classes of XSS attacks, but as far as this hole is concerned, the unsafe cat is already out of the bag by then*.
I don’t think this approach to security is well-thought-out. Unfortunately, in today’s never-mind-the-long-term-let’s-just-implement atmosphere (a similar problem is seen in JSONRequest), it’s too easy to ship things that will have lasting negative impact, and then standardise them because they’re “current practice”. I’m reminded of the way that Cookies were pushed onto the world.
A Modest Proposal
HTMLFormElement.submit() is clearly at odds with the Web architecture.
The right thing to do is to require user interaction before sending any unsafe method. For scripted requests, you could say that a request to the same site is exempt, perhaps as a configurable option; there could also be a whitelist of allowed sites for automatic POSTs.
In this way, you’re getting permission from the user before the request itself, in a way that’s conceptually similar to the way that most browsers store credentials now (you know, like “save this password.”).
Now, I’m not claiming that this approach will solve all of the security problems to do with cross-site scripting; clearly there’s more than safety involved. However, having a consistent, simple model for interaction on the Web is a whole lot better than piling hack upon hack (which is often a clever solution to a problem in a product, but a ticking time bomb in a longer-lived standard).
Of course, this would take some time to roll out, but at the current pace of innovation (especially with IE7 on the horizon), it would take perhaps 3-5 years before sites could start to rely on this behaviour. The alternative future is one where there are multiple holes and sites have to take truly convoluted measures to overcome them, or just give up on using forms altogether.
I’ve talked to a few people about this issue and raised it to the www-dom mailing list, even though the Working Group doesn’t exist any more; there’s discussion of doing some more work in DOM, so there is an opportunity to fix it. In the meantime, there’s nothing stopping implementations from fixing it; when reading the HTML and DOM specs together, the right thing to do is clear. Having this interaction noted in the DOM specification itself would just make it harder to use as an excuse for other holes.
- Ironically, there are other people in the Web APIs Working Group arguing that XmlHttpRequest should be able to change the Referer header — even though using it is one of the ways to mitigate this attack.
