strict-forgery-protection 0.0.4 → 0.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,29 +3,37 @@ module ForgeryProtection
3
3
 
4
4
  module ControllerExtension
5
5
  def self.included(controller)
6
- controller.around_filter :verify_strict_authenticity
6
+ controller.around_filter :detect_unverified_db_update, :if => proc { |c| c.protect_against_forgery? }
7
7
 
8
- def controller.skip_forgery_protection(*args)
9
- skip_filter :verify_authenticity_token, *args
10
- skip_filter :verify_strict_authenticity, *args
8
+ def controller.permit_unverified_state_changes(*args)
9
+ skip_filter :detect_unverified_db_update, *args
11
10
  end
12
11
  end
13
12
 
14
- private
13
+ protected
15
14
 
16
- def verify_strict_authenticity
15
+ def verify_authenticity_token
16
+ super.tap { @forgery_protection_invoked = true }
17
+ end
18
+
19
+ def detect_unverified_db_update
17
20
  ForgeryProtection::QueryTracker.reset_sql_events
18
21
 
19
22
  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
23
+ if ForgeryProtection::QueryTracker.sql_events.any? { |e| e.write? }
24
+ raise AttemptError, "A database update occurred for an unverified request" unless valid_forgery_protection_token?
25
+ raise AttemptError, "A database update occured but forgery protection seems disabled" unless forgery_protection_invoked?
26
+ end
24
27
  end
25
28
  end
26
29
 
27
- def handle_unverified_request
28
- raise AttemptError
30
+ def forgery_protection_invoked?
31
+ !!@forgery_protection_invoked
32
+ end
33
+
34
+ def valid_forgery_protection_token?
35
+ form_authenticity_token == params[request_forgery_protection_token] ||
36
+ form_authenticity_token == request.headers['X-CSRF-Token']
29
37
  end
30
38
  end
31
39
  end
@@ -1,3 +1,3 @@
1
1
  module ForgeryProtection
2
- VERSION = '0.0.4'.freeze
2
+ VERSION = '0.0.7'
3
3
  end
@@ -4,22 +4,22 @@ require 'action_dispatch'
4
4
 
5
5
  class ControllerTest < ActionController::TestCase
6
6
  class Controller < ActionController::Base
7
- skip_forgery_protection :only => :touch
7
+ protect_from_forgery :except => [ :unprotected_read, :unprotected_write ]
8
+ permit_unverified_state_changes :only => [ :db_permitted_read, :db_permitted_write ]
8
9
 
9
10
  def read
10
11
  render :json => post
11
12
  end
12
13
 
13
- def update
14
- post.update_attributes! :message => params[:message]
14
+ def write
15
+ post.update_attribute :message, params[:message]
15
16
 
16
17
  render :json => post
17
18
  end
18
19
 
19
- def touch
20
- post.touch
21
-
22
- render :json => post
20
+ %w(unprotected db_permitted).each do |prefix|
21
+ define_method("#{prefix}_read") { read }
22
+ define_method("#{prefix}_write") { write }
23
23
  end
24
24
 
25
25
  private
@@ -48,33 +48,61 @@ class ControllerTest < ActionController::TestCase
48
48
  end
49
49
 
50
50
  def test_reads
51
- get :read, :id => Post.last
51
+ %w(read unprotected_read db_permitted_read).each do |read_action|
52
+ get read_action, :id => Post.last
53
+ end
52
54
  end
53
55
 
54
- def test_verified_get
55
- assert_nothing_raised { get :update, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
56
+ def test_verified_get_write
57
+ assert_nothing_raised { get :write, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
56
58
 
57
59
  assert_equal 'bye', Post.last.message
58
60
  end
59
61
 
60
- def test_verified_post
61
- assert_nothing_raised { post :update, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
62
+ def test_verified_post_write
63
+ assert_nothing_raised { post :write, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye' }
62
64
 
63
65
  assert_equal 'bye', Post.last.message
64
66
  end
65
67
 
66
- def test_unverified_get
67
- assert_raises(ForgeryProtection::AttemptError) { get :update, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
68
+ def test_unverified_get_write
69
+ assert_raises(ForgeryProtection::AttemptError) { get :write, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
68
70
  end
69
71
 
70
- def test_unverified_post
71
- assert_raises(ForgeryProtection::AttemptError) { post :update, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
72
+ def test_unverified_post_write
73
+ assert_raises(ForgeryProtection::AttemptError) { post :write, :id => Post.last, :authenticity_token => 'bad token', :message => 'bye' }
74
+ end
75
+
76
+ def test_unprotected_writes
77
+ # Unfortunately tripping up developers for just POSTs to bad token with protection disabled is not enough:
78
+ # They are very likely to make the requests via the form, which Rails will include a valid CSRF token for.
79
+ #
80
+ # This would cause this csrf vulnerability to be exposed only when a real attacker attempts to compromise
81
+ # production. Since our checks are done after the action executed, the error would be raised too late. Hence
82
+ # we try to trip up developers when they disable forgery protection and make state changing calls, even if
83
+ # a valid csrf token is specified.
84
+ assert_raises(ForgeryProtection::AttemptError) do
85
+ get :unprotected_write, :id => Post.last, :authenticity_token => @csrf_token, :message => 'bye'
86
+ end
87
+
88
+ assert_raises(ForgeryProtection::AttemptError) do
89
+ post :unprotected_write, :id => Post.last, :authenticity_token => @csrf_token, :message => 'good bye'
90
+ end
72
91
  end
73
92
 
74
- def test_skipped_verification
93
+ def test_db_permitted_verification_write
75
94
  before = Post.last.updated_at
76
- assert_nothing_raised { get :touch, :id => Post.last }
95
+ assert_nothing_raised { get :db_permitted_write, :id => Post.last }
77
96
 
78
97
  assert_not_equal before, Post.last.updated_at, "Should update the record"
79
98
  end
99
+
100
+ def test_global_forgery_disabled
101
+ @controller.allow_forgery_protection = false
102
+
103
+ assert_nothing_raised do
104
+ get :write, :id => Post.last, :message => 'get bye'
105
+ post :write, :id => Post.last, :message => 'post bye'
106
+ end
107
+ end
80
108
  end
data/test/test.sqlite3 CHANGED
Binary file
metadata CHANGED
@@ -1,77 +1,71 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: strict-forgery-protection
3
- version: !ruby/object:Gem::Version
4
- prerelease:
5
- version: 0.0.4
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.7
6
6
  platform: ruby
7
- authors:
8
- - Dmitry Ratnikov
9
- autorequire:
7
+ authors:
8
+ - Dmitry Ratnikov
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
-
13
- date: 2012-04-19 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"
24
- type: :runtime
25
- version_requirements: *id001
26
- description:
27
- email:
28
- - ratnikov@google.com
12
+ date: 2012-05-10 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ version_requirements: &2056 !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ~>
19
+ - !ruby/object:Gem::Version
20
+ version: '3.1'
21
+ none: false
22
+ requirement: *2056
23
+ prerelease: false
24
+ type: :runtime
25
+ description:
26
+ email:
27
+ - ratnikov@google.com
29
28
  executables: []
30
-
31
29
  extensions: []
32
-
33
30
  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
31
+ files:
32
+ - lib/strict-forgery-protection.rb
33
+ - lib/forgery_protection/controller_extension.rb
34
+ - lib/forgery_protection/instrumenter_extension.rb
35
+ - lib/forgery_protection/query_tracker.rb
36
+ - lib/forgery_protection/sql_event.rb
37
+ - lib/forgery_protection/version.rb
38
+ - test/controller_test.rb
39
+ - test/db_events_test.rb
40
+ - test/test.sqlite3
41
+ - test/test_helper.rb
46
42
  homepage: http://github.com/ratnikov/strict-forgery-protection
47
43
  licenses: []
48
-
49
- post_install_message:
44
+ post_install_message:
50
45
  rdoc_options: []
51
-
52
- require_paths:
53
- - lib
54
- required_ruby_version: !ruby/object:Gem::Requirement
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ! '>='
51
+ - !ruby/object:Gem::Version
52
+ version: '0'
55
53
  none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: "0"
60
- required_rubygems_version: !ruby/object:Gem::Requirement
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
61
59
  none: false
62
- requirements:
63
- - - ">="
64
- - !ruby/object:Gem::Version
65
- version: "0"
66
60
  requirements: []
67
-
68
- rubyforge_project:
61
+ rubyforge_project:
69
62
  rubygems_version: 1.8.15
70
- signing_key:
63
+ signing_key:
71
64
  specification_version: 3
72
65
  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
66
+ test_files:
67
+ - test/controller_test.rb
68
+ - test/db_events_test.rb
69
+ - test/test.sqlite3
70
+ - test/test_helper.rb
71
+ ...