Browse files

add initial CSRF protection implementation

  • Loading branch information...
1 parent a8fdea0 commit deadf7a2bb894f99d717130e2bef8d8b67085903 @rkh committed Apr 26, 2011
Showing with 176 additions and 0 deletions.
  1. +127 −0 lib/sinatra/csrf.rb
  2. +49 −0 spec/csrf_spec.rb
View
127 lib/sinatra/csrf.rb
@@ -0,0 +1,127 @@
+require 'sinatra/base'
+require 'forwardable'
+
+module Sinatra
+ module CSRF
+ SAFE_METHODS = %w[GET HEAD OPTIONS TRACE]
+ TOKEN_HEADER = 'HTTP_X_CSRF_Token'
+ TOKEN_FIELD = 'authenticity_token'
+
+ def self.registered(base)
+ base.enable :csrf_protection unless base.respond_to? :csrf_protection
+ base.helpers Helpers
+ base.use Middleware, base
+ end
+
+ module Helpers
+ def authenticity_token
+ session['sinatra.token']
+ end
+
+ def authenticity_tag
+ return "" unless authenticity_token
+ "<input type='hidden' name='#{TOKEN_FIELD}' value='#{authenticity_token}' />"
+ end
+
+ alias csrf_token authenticity_token
+ alias csrf_tag authenticity_tag
+ end
+
+ class Middleware
+ extend Forwardable
+ def_delegators :@base, :csrf_protection?, :csrf_protection, :test?, :development?
+
+ def initialize(app, base)
+ @app, @base = app, base
+ end
+
+ def call(env)
+ return @app.call(env) unless csrf_protection
+ request = Sinatra::Request.new env
+ set_token(request) if checks.include? :token
+ safe?(request) ? @app.call(env) : response(request)
+ end
+
+ private
+
+ def checks
+ return @checks if defined? @checks
+ checks = [:verb, Array(*csrf_protection)]
+ checks.map! { |c| c == true ? :optional_referrer : c }
+ checks.delete :verb if checks.delete :all_verbs
+ @checks = checks
+ end
+
+ def set_token(request)
+ request.session['sinatra.token'] ||= '%x' % rand(2**255)
+ end
+
+ def safe?(r)
+ checks.any? { |c| send("safe_#{m}?", r) }
+ end
+
+ def safe_method?(r)
+ SAFE_METHODS.include? r.request_method
+ end
+
+ def safe_token?(r)
+ token = request.session['sinatra.token']
+ r.env[TOKEN_HEADER] == token or r[TOKEN_FIELD] == token
+ end
+
+ def safe_forms?(r)
+ request.xhr? or safe_token?(r)
+ end
+
+ def safe_referrer?(r)
+ URI.parse(r.referrer.to_s).host == r.host
+ end
+
+ alias safe_referer? safe_referrer?
+
+ def safe_optional_referrer?(r)
+ r.referrer.nil? or safe_referrer?(r)
+ end
+
+ def response(r)
+ fail error if test?
+ response = Rack::Response.new
+ response.status = 412
+ if development?
+ response.body << <<-HTML.gsub(/^ {12}/, '')
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <style type="text/css">
+ body { text-align:center;font-family:helvetica,arial;font-size:22px;
+ color:#888;margin:20px}
+ #c {margin:0 auto;width:500px;text-align:left}
+ </style>
+ </head>
+ <body>
+ <h2>Potentinal CSRF attack prevented!</h2>
+ <img src='#{r.script_name}/__sinatra__/500.png'>
+ <div id="c">
+ <p>
+ Sinatra automatically blocks unsafe requests coming from other
+ hosts. If you want to allow such requests, please make sure
+ you fully understand how CSRF attacks work first, and then,
+ add the following line to your Sinatra application:
+ </p>
+ <pre>disable :csrf_protection</pre>
+ <p>
+ You can also change the CSRF counter measures like this:
+ </p>
+ <pre>set :csrf_protection, :token</pre>
+ </div>
+ </body>
+ </html>
+ HTML
+ end
+ response.finish
+ end
+ end
+ end
+
+ register CSRF
+end
View
49 spec/csrf_spec.rb
@@ -0,0 +1,49 @@
+require 'backports'
+require_relative 'spec_helper'
+
+describe Sinatra::CSRF do
+ safe = %w[get options]
+ unsafe = %w[post put delete]
+ unsafe << "patch" if Sinatra::Base.respond_to? :patch
+
+ before do
+ mock_app do
+ register Sinatra::CSRF
+ set :csrf_protection, checks
+ (safe + unsafe).each { |v| send(v, '/') { 'ok' }}
+ get('/token') { authenticity_token }
+ get('/tag') { authenticity_tag }
+ end
+ end
+
+ describe 'optional referrer' do
+ let(:checks) { :optional_referrer }
+ it 'allows get request'
+ it 'prevents requests from a different host'
+ it 'allows requests from the same host'
+ it 'allows requests with no referrer'
+ end
+
+ describe 'referrer' do
+ let(:checks) { :referrer }
+ it 'prevents requests from a different host'
+ it 'allows requests from the same host'
+ it 'prevents requests with no referrer'
+ end
+
+ describe 'token' do
+ let(:checks) { :token }
+ it 'prevents normal requests without a valid token'
+ it 'prevents ajax requests without a valid token'
+ it 'allows normal requests with a valid token'
+ it 'allows ajax requests with a valid token'
+ end
+
+ describe 'form' do
+ let(:checks) { :form }
+ it 'prevents normal requests without a valid token'
+ it 'allows ajax requests without a valid token'
+ it 'allows normal requests with a valid token'
+ it 'allows ajax requests with a valid token'
+ end
+end

0 comments on commit deadf7a

Please sign in to comment.