8.5. The Use and Abuse of HTTP MethodsIn the spring of 2005, Google introduced a browser plug-in called Google Web Accelerator (GWA), which set off heated discussions in the Rails community. The reason is that GWA worked by pre-fetching links. Upon loading a page, GWA would scan it for links and load them before they were even clickedso when the user did click, the next page would already be cached and load much faster. The problem was that many Rails applications (including Basecamp, the original Rails application) used regular links for destructive actions, such as "delete this post." So if you installed GWA and then visited your Basecamp account, the plug-in triggered a wave of data loss. Users and developers alike were understandably quite upset by the unintended consequences. Google quickly cancelled the product in response to the uproar. But technically, the plug-in wasn't doing anything wrong (besides being wasteful with bandwidth). GWA was only creating HTTP GET requests, which, according the spec, are supposed to be safe for intermediaries like GWA to use. The real problem was that Rails developers had adopted the bad habit of using GET to trigger deletes. The lesson was hard-learned, but important. Today, Rails is leading the charge among web frameworks to support the full vocabulary of HTTP methods, beyond just GET and POST. With most helpers, the fix is as simple as providing a :method option. For example, to create a proper delete link: <%= link_to 'Delete Contact', contact_url(:id => contact), :method => :delete %> Instead of creating a standard link, this helper will create a JavaScript linkone that looks just the same, but has a script in the onclick attribute. The script jumps through the necessary hoops to send the right request. Because browsers generally don't support the DELETE method, Rails piggybacks on the POST method by sending an extra parameter (_method) along with the request. It's not ideal, but it's an acceptable stopgap solution until browsers support more methods. The output of the above helper is this: <a href="/contacts/1" onclick="var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href; var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m); f.submit( ); return false;">Delete Contact</a> Upon clicking the link, the JavaScript actually creates a new hidden form and input field and submits it. The effect is totally transparent to the end user, but as far as Rails is concerned, the incoming request is a full-fledged HTTP DELETE request. This brings us to the second half of the equation, the server side. Employing JavaScript to use the correct request method is nice, but if your destroy action still responds to GET, you're still vulnerable. There are several ways to tackle the problem. From within an action, the request object represents all that's known about the current request. So to find out the request method, you'd use (shockingly) request.method. The value will be one of five symbols: :get, :post, :put, :delete, and :head. The request object also provides corresponding Boolean "question-mark" methods, such as request.get? and request.post?. For example, consider account confirmation, a common feature of web applications. In order to deter spammers, new users are emailed a confirmation link, which they're supposed to click before the account is activated. Most implementations of this pattern are flawed, because they use GET requests to change state on the server. A better approach is to check the request method and show a confirmation form if the incoming request is a GET. That kind of conditional processing is made easy by request.post? and friends: def confirm @user = User.find_by_token params[:id] if request.post? @user.update_attributes :confirmed => true redirect_to home_url else render :inline => %Q(<%= start_form_tag %> <%= submit_tag "Confirm Account" %> <%= end_form_tag %>) end end Alternatively, verify, a specialized kind of before_filter, can be used to limit which request methods are allowed for each action. Options provided to verify will determine what happens if the conditions aren't met, such as redirecting and adding a flash. For example: class UsersController < ApplicationController verify :only => :confirm, :method => :post, :add_flash => { "notice" => "Please confirm your account." }, :redirect_to => :confirm_form def confirm_form render :inline => %Q(<%= start_form_tag %> <%= submit_tag "Confirm" %> <%= end_form_tag %>) end # only POSTS will be able to reach this action def confirm @user = User.find_by_token params[:id] @user.update_attributes :confirmed => true redirect_to home_url end end Another solution is to use routes. For example: # only matches if the request method is GET map.connect "/confirm/:id", :controller => "users", :action => "confirm_form", :conditions => { :method => :get } # only matches if the request method is POST map.connect "/confirm/:id", :controller => "users", :action => "confirm", :conditions => { :method => :post } In many cases, you can automatically get the benefits of the :conditions option by using map.resources. For example: ActionController::Routing::Routes.draw do |map| map.resources :products map.connect ':controller/:action/:id' end The resources method generates a whole slew of named routes, and it uses :conditions to direct the same path to multiple actions, depending on the HTTP method. Table 8-1 shows all of the routes generated by map.resources :products.
|