Taking the unsafe GETs out of Rails
Posted by Tom Moertel Sun, 08 May 2005 16:00:00 GMT
Update 2005-06-17: The button_to helper, introduced below, has been incorporated into the Rails framework and will be a part of the Rails 1.0 release. See Good news: The button_to helper is now part of Rails! for more.
Update 2005-05-28: I now have a more-recent version of the button_to code, which adds support for the disabled HTML attribute. Thanks to Sean T Allen for the great idea and initial implementation.
As I wrote earlier, it’s time for web developers to do away with the fundamentally broken practice of using hypertext links to trigger dangerous events such as deleting things. One of the first places we ought to clean house is in the burgeoning Rails web-application framework, where this practice is pervasive.
The primary culprit in Rails is the all-too-easy link_to method, which is (presently) the orthodox means of creating links to any action, even unsafe ones. For example:
link_to "Destroy", :controller => 'accounts',
:action => 'destroy', :id => 6
The above code generates the following HTML hypertext link, which when followed will merrily delete account number 6:
<a href="/accounts/destroy/6">Destroy</a>
Because this practice is dangerous and contrary to the decade-old convention that links be safe, the link_to method thoughtfully lets us request that a Javascript confirmation dialog be tacked onto the link for added protection:
link_to "Destroy", ..., :confirm => "Are you sure?"
The resulting “safe” HTML:
<a href="/accounts/destroy/6"
onclick="return confirm('Are you sure?');">Destroy</a>
Unfortunately, the Javascript protection doesn’t work. First, not all web browsers care about it. Lots of people surf with Javascript turned off. Second, a whole slew of things besides web browsers live on the Internet, and almost all of them are oblivious to Javascript. Web crawlers fall into this category. They will be more than happy to follow any link you feed to them. “Hey, Googlebot just deleted every account in our database!” Oops.
Thus another layer of protection is commonly used: authorization. The theory is that dangerous links can be safely corralled in the private parts of a web application, where the public and web crawlers cannot go. Only authorized users can get into those parts, and those users will be smart enough not to click on the truly dangerous links unless they really mean it.
The problem is, any number of intermediary agents can be operating on behalf of an authorized user, and these agents are free to do anything the user is allowed to do, such as follow dangerous links. Google’s Web Accelerator is one such agent. It tries to make your surfing faster by (among other things) pre-fetching the resources that are linked to on the pages you visit. And what happens if you, an authorized user, visit a page containing dangerous links? That’s right, Web Accelerator will fetch the “resources” those links point to – and delete a bunch of your stuff.
I hope by this point that I have argued convincingly that using links for unsafe actions is a bad idea. Even if you feel justified in ignoring the applicable parts of the HTTP RFCs, it’s a bad idea. Even if you tack on Javascript confirmations and hide your links in authorization-protected zones of your site, it’s a bad idea. It is, all around, a bad idea. Don’t do it.
So what alternatives are there? Read on for one possibility, button_to.
A link_to alternative: button_to
If you shouldn’t use links for unsafe actions, what should you use instead? Form buttons. Forms can be submitted via HTTP POST requests, and POST requests are understood to do potentially unsafe things. Web crawlers will not try to click your buttons. Intermediary user agents will not try to pre-submit your forms.
So, how do we make doing the right thing as easy as creating a link? My answer is button_to, a method that takes the same parameters as the ever-popular link_to but creates a tiny form that contains a single button instead of a link:
button_to "Destroy", { :action => 'destroy', :id => 6 },
:confirm => "Are you sure?"
The resulting HTML (reformatted for your viewing pleasure):
<form method="post" action="/accounts/destroy/6" class="button-to">
<div><input onclick="return confirm('Are you sure?');"
value="Destroy" type="submit">
</div>
</form>
The forms I create are given the class button-to, which makes it easy to apply styles to them. With a little work, the buttons can look pretty darn good:

So that’s my plea: Use a button. It’s a simple solution to a potentially ugly problem. There’s no need for Ajax or other non-portable Javascript trickery. Just use a button.
And it’s easy, too. In a few minutes, I was able to “clean house” on the Rails application I’m developing.
The code
If you’re interested, here’s the code for button_to. It’s only ten lines, but the docs make it look much longer.
# Generates a form containing a sole button that submits to the URL
# given by _options_. Use this method instead of +link_to+ for
# dangerous actions that do not have the safe HTTP GET semantics
# implied by using a hypertext link.
#
# The parameters are the same as for +url_to+. Any _html_options_
# that you pass will be applied to the inner +input+ element. The
# generated form element is given the class 'button-to', to which
# you can attach CSS styles for display purposes.
#
# Example 1:
#
# # inside of controller 'feeds'
# button_to "Edit", :action => 'edit', :id => 3
#
# Generates the following HTML (sans formatting):
#
# <form method="post" action="/feeds/edit/3" class="button-to">
# <div><input value="Edit" type="submit"></div>
# </form>
#
# Example 2:
#
# button_to "Destroy", { :action => 'destroy', :id => 3 },
# :confirm => "Are you sure?"
#
# Generates the following HTML (sans formatting):
#
# <form method="post" action="/feeds/destroy/3" class="button-to">
# <div><input onclick="return confirm('Are you sure?');"
# value="Destroy" type="submit">
# </div>
# </form>
#
# *NOTE*: This method generates HTML code that represents a form.
# Forms are "block" content, which means that you should not try to
# insert them into your HTML where only inline content is expected.
# For example, you can legally insert a form inside of a +div+ or +td+
# element or in between +p+ elements, but not in the middle of a run
# of text. (Bottom line: Always validate your HTML before going
# public, especially if this paragraph seems confusing.)
def button_to(name, options = {}, html_options = nil)
html_options = (html_options || {}).stringify_keys
convert_confirm_option_to_javascript!(html_options)
url, name = options.is_a?(String) ?
[ options, name || options ] :
[ url_for(options), name || url_for(options) ]
html_options.merge!("type" => "submit", "value" => name)
"<form method='post' action='#{h url}' class='button-to'><div>" +
tag("input", html_options) + "</div></form>"
end
Thanks for reading and happy unsafe-link hunting!

This is great, great, great – except for one thing: you can’t nest forms inside other forms. While this will work fine for many cases, any time you want a button_to button inside an already existing form things will break in unfortunate ways.
It’s still worth having of course, but the rather nasty edge case needs to be documented.
Simon, thanks for your comment. I had not considered the form-in-a-form case. But, now that I think about it, I don’t think it’s as limiting as it might seem. Let me explain.
When we want to trigger an action and we’re already inside of a form, there are really two cases to consider. The first case is when the action depends on the state of the entire form. This case is actually the common case for plain old forms: Click a submission button and the entire form is processed. For this case, just create a submission button with the usual X/HTML markup:
No need to use button_to or anything like it. Normal markup works just fine.
The second case is the interesting one. It’s when we want the action to operate only upon local state, acting in effect as an escape hatch from the form. (E.g., consider the Add To Cart buttons in Amazon Wish Lists.) That is, we do not care about the state of the entire form, just the local context of the action (e.g., a particular item in the wish list to add to the cart). This is where many people turn to hypertext links, encoding the local state into the URL. But there are other options.
One option is to use a normal form-submission button, just like in the common case, but with a twist: We additionally encode the local state into the button (e.g., via its name attribute). When the server processes the request, it simply ignores the form state and processes the encoded local state. The downside is that if the form state is large, the POST requests will be correspondingly wasteful. Nevertheless, in many cases the waste may be within acceptable limits.
Another option is to use a link inside of the form to confirm – not to trigger – the local action. The link points to a confirmation page that summarizes the action to be performed in a form. If the user confirms the action, the form is posted, triggering the action. The upside is that if the action is potentially unsafe, users will have a real opportunity to confirm it (which Javascript-based approaches do not guarantee). The downside is that if the typical usage scenario involves a user having to perform the action frequently, the confirmation round trips may become burdensome. (On the other hand, if that’s the typical scenario, we might be better off redesigning the interface to allow for batch operations.)
Yes, the corner case does exist. But it’s not as big as it initially seems.
Cheers,
Tom
You’re preaching to the choir about GET-based deletions being a bad idea, and the idea of “safetying” them with JavaScript is laughable. In fact, this exact sort of problem was featured on TheDailyWTF.com many months ago:
http://thedailywtf.com/forums/65974/ShowPost.aspx
Has anyone told DHH about the Daily WTF?
Whoops, just noticed the date on your blog post (having just come here from reddit.com). Kudos to you for spotting the GET problem way before others.
Regardless of when you said it, I still agree with your point. :)
(Saw this on reddit, just wanted people to know it’s been a non issue for quite awhile…)
How Rails is prepared for GWA II: Vengeance. At a controller level you can use
verify :method => :post, :only => %w( delete update )which ensures that you’re posting to your destructive actions, and at a view levellink_to('post me', {url_options}, :method => :post)(which lets you post to destructive actions).great, could i ask that if i want to personalise my look and feel style of button, am i supposed to overload this method directly in my application helper or corresponding controller helper?
pointer—If you want to personalize the look and feel of the buttons, use Cascading Style Sheets. The outer
formelement ofbutton_to-created buttons has the class “button-to”, which makes a convenient hook for attaching styles.—Tom
This is a great article.
Hey what would be the equivalent of a link_to_remote ?
Thanks for a great article, helped me out.