Permalink
Browse files

Security: Force all GET requests to be read-only

The W3C makes a clear distinction between GET and POST requests.  GET
requests should only cause "safe" actions, and the user should never be
held accountable for making GET requests.  See the following for an
overview:

  http://www.w3.org/2001/tag/doc/whenToUseGet.html

The Rails 'protect_against_forgery' function (and possibly some web
browsers) rely on the distinction between GET and POST to provide
protection against CSRF attacks.  See:

  http://en.wikipedia.org/wiki/Cross-site_request_forgery
  http://guides.rubyonrails.org/security.html#_csrf_countermeasures

Unfortunately, enforcing these rules in rather difficult, especially in
a large application with lots of controllers and plugins.  So this patch
applies a rather heavy-handed fix: We globally block database writes
during GET requests, and specifically override that policy in one or two
places.

All of our current overrides invoke User#reset_token!.  I haven't
performed a full security analysis of allowing User#reset_token! (or
updates to session[:user] based on our "remember me" token) in a GET
request.  For now, I'm going to go ahead and allow this activity--if we
actually have some sort of vulnerability here, it affects a wide range
of web applications.

Note that this patch may break some part of the /admin interface.  I've
tried posting articles and other basic stuff, but I haven't used the
lesser-known corners of /admin since making these changes.  Please
report any problems.
  • Loading branch information...
emk committed Dec 20, 2008
1 parent ff0113a commit c500bf8e05c250d02672c30d079a0bdeb66f0569
@@ -43,7 +43,10 @@ def forget
def activate
self.current_user = site.user_by_token(params[:id])
if logged_in?
current_user.reset_token!
# TODO - See security comments on AuthenticatedSystem#login_from_cookie.
ActiveRecord::Base.with_writable_records do
current_user.reset_token!
end
else
flash[:error] = "Invalid token. Try resending your forgotten password request."
end
@@ -3,6 +3,7 @@ class Admin::AssetsController < Admin::BaseController
skip_before_filter :login_required
before_filter :find_asset, :except => [:index, :new, :create, :latest, :search, :upload, :clear_bucket]
before_filter :login_required
before_filter :protect_action, :except => [:index, :new, :latest, :search]
def index
search_assets 24
@@ -3,6 +3,7 @@ class ApplicationController < ActionController::Base
include Mephisto::CachingMethods
before_filter :set_cache_root
around_filter :get_requests_are_readonly
helper_method :site
attr_reader :site
@@ -95,6 +96,21 @@ def set_cache_root
end
end
# Much of a web browser's built-in protection against CSRF attacks
# assumes that all GET requests are "safe". Since we don't want to
# rely on getting this 100% right in every controller and plugin, let's
# just enforce this policy globally. You can override this using
# <code>skip_filter :get_requests_are_readonly</code>.
def get_requests_are_readonly
if request.method == :get
ActiveRecord::Base.with_readonly_records do
yield
end
else
yield
end
end
def with_site_timezone
old_tz = ENV['TZ']
ENV['TZ'] = site.timezone.name
@@ -0,0 +1,35 @@
module ActiveRecord
class Base
# Are ActiveRecord::Base objects currently readonly?
def self.all_records_are_readonly?
Thread.current[:all_records_are_readonly]
end
def readonly_with_global_flag?
self.class.all_records_are_readonly? || readonly_without_global_flag?
end
alias_method_chain :readonly?, :global_flag
# Make all ActiveRecord::Base objects readonly within a block.
def self.with_readonly_records # :yield:
saved = all_records_are_readonly?
begin
Thread.current[:all_records_are_readonly] = true
yield
ensure
Thread.current[:all_records_are_readonly] = saved
end
end
# Make all ActiveRecord::Base objects writable within a block.
def self.with_writable_records # :yield:
saved = all_records_are_readonly?
begin
Thread.current[:all_records_are_readonly] = false
yield
ensure
Thread.current[:all_records_are_readonly] = saved
end
end
end
end
@@ -74,7 +74,17 @@ def self.included(base)
def login_from_cookie
return unless cookies[:token] && !logged_in?
self.current_user = site.user_by_token(cookies[:token])
cookies[:token] = { :value => self.current_user.reset_token! , :expires => self.current_user.token_expires_at } if logged_in?
# TODO - We allow the token to be changed on GET requests and we log
# the user in. I haven't fully analyzed the consequences of allowing
# session and token updates on hostile GET requests triggered by CSRF
# attacks. If this helps out in some kind of attack, it would affect
# almost every single web application in existence.
ActiveRecord::Base.with_writable_records do
cookies[:token] = {
:value => self.current_user.reset_token!,
:expires => self.current_user.token_expires_at
} if logged_in?
end
true
end
@@ -0,0 +1,35 @@
require File.dirname(__FILE__) + '/../spec_helper'
# Verify that our readonly_record patches are working.
describe "Any record" do
before :each do
@article = Article.make(:title => "Original title")
end
it "should be writable by default" do
assert !ActiveRecord::Base.all_records_are_readonly?
@article.title = "Hello!"
@article.save!
end
it "should not be writable inside with_readonly_records" do
assert_raise ActiveRecord::ReadOnlyRecord do
ActiveRecord::Base.with_readonly_records do
assert ActiveRecord::Base.all_records_are_readonly?
@article.title = "Hello!"
@article.save!
end
end
end
it "should be writable inside with_writable_records" do
ActiveRecord::Base.with_readonly_records do
ActiveRecord::Base.with_writable_records do
assert !ActiveRecord::Base.all_records_are_readonly?
@article.title = "Hello!"
@article.save!
end
end
end
end

0 comments on commit c500bf8

Please sign in to comment.