strict-forgery-protection 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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