Taking the unsafe GETs out of Rails

Posted on
Tags: gwa, rails, get, post, link-to, safe, unsafe

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.

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">

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:

screen capture showing Edit and Destroy buttons created with the button_to method
screen capture showing Edit and Destroy buttons created with the button_to method

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
  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>"

Thanks for reading and happy unsafe-link hunting!

comments powered by Disqus