strict-forgery-protection 0.0.4 → 0.0.7

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.
@@ -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
+ ...