strict-forgery-protection 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ module ForgeryProtection
2
+ class AttemptError < StandardError; end
3
+
4
+ module ControllerExtension
5
+ def self.included(controller)
6
+ controller.around_filter :verify_strict_authenticity
7
+
8
+ def controller.skip_forgery_protection(*args)
9
+ skip_filter :verify_authenticity_token, *args
10
+ skip_filter :verify_strict_authenticity, *args
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def verify_strict_authenticity
17
+ ForgeryProtection::QueryTracker.reset_sql_events
18
+
19
+ yield.tap do
20
+ provided_tokens = [ request.headers['X-CSRF-Token'], params[request_forgery_protection_token] ].compact
21
+ if ForgeryProtection::QueryTracker.sql_events.any? { |e| e.write? } && !provided_tokens.include?(form_authenticity_token)
22
+ handle_unverified_request
23
+ end
24
+ end
25
+ end
26
+
27
+ def handle_unverified_request
28
+ raise AttemptError
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ class ActiveSupport::Notifications::Instrumenter
2
+ def instrument(name, payload={})
3
+ started = Time.now
4
+
5
+ begin
6
+ yield.tap { |result| payload[:result] = result }
7
+ rescue Exception => e
8
+ payload[:exception] = [e.class.name, e.message]
9
+ raise e
10
+ ensure
11
+ @notifier.publish(name, started, Time.now, @id, payload)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ require 'forgery_protection/instrumenter_extension'
2
+ require 'forgery_protection/sql_event'
3
+
4
+ module ForgeryProtection
5
+ class QueryTracker
6
+ def self.record_sql_event(event)
7
+ sql_events << event
8
+ end
9
+
10
+ def self.sql_events
11
+ Thread.current['active_record_sql_events'] ||= []
12
+ end
13
+
14
+ def self.reset_sql_events
15
+ sql_events.clear
16
+ end
17
+
18
+ def call(*args)
19
+ self.class.record_sql_event SqlEvent.new(*args)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'active_support'
2
+
3
+ module ForgeryProtection
4
+ class SqlEvent < ActiveSupport::Notifications::Event
5
+ def read?
6
+ result.respond_to?(:each)
7
+ end
8
+
9
+ def write?
10
+ !read?
11
+ end
12
+
13
+ private
14
+
15
+ def result
16
+ payload[:result]
17
+ end
18
+
19
+ def sql
20
+ payload[:sql]
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module ForgeryProtection
2
+ VERSION = '0.0.2'.freeze
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'forgery_protection/query_tracker'
2
+ require 'forgery_protection/controller_extension'
3
+
4
+ ActiveSupport.on_load(:active_record) do
5
+ ActiveSupport::Notifications.notifier.subscribe 'sql.active_record', ForgeryProtection::QueryTracker.new
6
+ end
7
+
8
+ ActiveSupport.on_load(:action_controller) do
9
+ include ForgeryProtection::ControllerExtension
10
+ end
11
+
@@ -0,0 +1,80 @@
1
+ require 'test_helper'
2
+
3
+ require 'action_dispatch'
4
+
5
+ class ControllerTest < ActionController::TestCase
6
+ class Controller < ActionController::Base
7
+ skip_forgery_protection :only => :touch
8
+
9
+ def read
10
+ render :json => post
11
+ end
12
+
13
+ def update
14
+ post.update_attributes! :message => params[:message]
15
+
16
+ render :json => post
17
+ end
18
+
19
+ def touch
20
+ post.touch
21
+
22
+ render :json => post
23
+ end
24
+
25
+ private
26
+
27
+ def post
28
+ Post.find params[:id]
29
+ end
30
+ end
31
+
32
+ tests Controller
33
+
34
+ setup do
35
+ @routes = ActionDispatch::Routing::RouteSet.new
36
+
37
+ @routes.draw do
38
+ match ':controller/:action(/:id)'
39
+ end
40
+
41
+ @controller.extend @routes.url_helpers
42
+
43
+ session[:_csrf_token] = @csrf_token = 'secret-csrf-token'
44
+
45
+ Post.delete_all
46
+
47
+ Post.create! :message => 'hello'
48
+ end
49
+
50
+ def test_reads
51
+ get :read, :id => Post.last
52
+ end
53
+
54
+ def test_verified_get
55
+ assert_nothing_raised { get :update, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
56
+
57
+ assert_equal 'bye', Post.last.message
58
+ end
59
+
60
+ def test_verified_post
61
+ assert_nothing_raised { post :update, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
62
+
63
+ assert_equal 'bye', Post.last.message
64
+ end
65
+
66
+ def test_unverified_get
67
+ assert_raises(ForgeryProtection::AttemptError) { get :update, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
68
+ end
69
+
70
+ def test_unverified_post
71
+ assert_raises(ForgeryProtection::AttemptError) { post :update, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
72
+ end
73
+
74
+ def test_skipped_verification
75
+ before = Post.last.updated_at
76
+ assert_nothing_raised { get :touch, :id => Post.last }
77
+
78
+ assert_not_equal before, Post.last.updated_at, "Should update the record"
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ require 'test_helper'
2
+
3
+ class DbEventsTest < ActiveSupport::TestCase
4
+ def test_read_queries
5
+ assert_reads do
6
+ Post.all
7
+ Post.count
8
+ Post.find_by_id 42
9
+ end
10
+ end
11
+
12
+ def test_raw_reads
13
+ assert_reads do
14
+ execute "SELECT * from posts"
15
+ execute "SELECT total_changes()"
16
+ end
17
+ end
18
+
19
+ def test_record_modification
20
+ assert_writes do
21
+ post = Post.create! :message => 'hello world'
22
+ Post.delete_all
23
+ post.update_attributes :message => 'goodbye'
24
+ post.destroy
25
+ end
26
+ end
27
+
28
+ def test_raw_writes
29
+ assert_writes do
30
+ execute "CREATE TABLE foos (foo INT)"
31
+ execute "INSERT INTO foos (foo) VALUES (5)"
32
+ execute "DELETE FROM foos"
33
+ execute "DROP TABLE foos"
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def assert_reads
40
+ previous_changes = total_changes
41
+
42
+ events = sql_events { yield }
43
+
44
+ assert events.all?(&:read?), "Expected all events to be read, but got: #{events.map { |e| [e, e.read? ] }.inspect}"
45
+
46
+ assert_equal 0, total_changes - previous_changes, "Expected no new changes"
47
+ end
48
+
49
+ def assert_writes
50
+ events = sql_events { yield }
51
+
52
+ assert events.all?(&:write?), "Expected all events to be write, but got: #{events.map { |e| [ e, e.write? ] }.inspect}"
53
+ end
54
+
55
+ def sql_events
56
+ ForgeryProtection::QueryTracker.reset_sql_events
57
+
58
+ yield
59
+
60
+ ForgeryProtection::QueryTracker.sql_events
61
+ end
62
+
63
+ def total_changes
64
+ execute("SELECT total_changes()").first['total_changes()']
65
+ end
66
+
67
+ def execute(sql)
68
+ ActiveRecord::Base.connection.execute sql
69
+ end
70
+ end
data/test/test.sqlite3 ADDED
Binary file
@@ -0,0 +1,30 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ require 'test/unit'
7
+
8
+ require 'active_support'
9
+ require 'active_record'
10
+ require 'action_controller'
11
+ require 'logger'
12
+
13
+ require 'strict-forgery-protection'
14
+
15
+ ActiveRecord::Base.logger = Logger.new('debug.log')
16
+ ActiveRecord::Base.establish_connection :adapter => 'sqlite3',
17
+ :database => File.expand_path('../test.sqlite3', __FILE__),
18
+ :timeout => 5000
19
+
20
+ ActiveRecord::Schema.define do
21
+ create_table :posts, :force => true do |t|
22
+ t.string :message
23
+
24
+ t.timestamps
25
+ end
26
+ end
27
+
28
+ class Post < ActiveRecord::Base
29
+ end
30
+
metadata ADDED
@@ -0,0 +1,77 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strict-forgery-protection
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.2
6
+ platform: ruby
7
+ authors:
8
+ - Dmitry Ratnikov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2012-02-09 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rails
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ~>
22
+ - !ruby/object:Gem::Version
23
+ version: 3.1.0
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ description:
27
+ email:
28
+ - ratnikov@google.com
29
+ executables: []
30
+
31
+ extensions: []
32
+
33
+ extra_rdoc_files: []
34
+
35
+ files:
36
+ - lib/strict-forgery-protection.rb
37
+ - lib/forgery_protection/controller_extension.rb
38
+ - lib/forgery_protection/instrumenter_extension.rb
39
+ - lib/forgery_protection/query_tracker.rb
40
+ - lib/forgery_protection/sql_event.rb
41
+ - lib/forgery_protection/version.rb
42
+ - test/controller_test.rb
43
+ - test/db_events_test.rb
44
+ - test/test.sqlite3
45
+ - test/test_helper.rb
46
+ homepage: http://github.com/ratnikov/strict-forgery-protection
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.9
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Extends Rails to be strict CSRF token protection
73
+ test_files:
74
+ - test/controller_test.rb
75
+ - test/db_events_test.rb
76
+ - test/test.sqlite3
77
+ - test/test_helper.rb