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.
- data/lib/forgery_protection/controller_extension.rb +20 -12
- data/lib/forgery_protection/version.rb +1 -1
- data/test/controller_test.rb +46 -18
- data/test/test.sqlite3 +0 -0
- metadata +55 -61
@@ -3,29 +3,37 @@ module ForgeryProtection
|
|
3
3
|
|
4
4
|
module ControllerExtension
|
5
5
|
def self.included(controller)
|
6
|
-
controller.around_filter :
|
6
|
+
controller.around_filter :detect_unverified_db_update, :if => proc { |c| c.protect_against_forgery? }
|
7
7
|
|
8
|
-
def controller.
|
9
|
-
skip_filter :
|
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
|
-
|
13
|
+
protected
|
15
14
|
|
16
|
-
def
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
28
|
-
|
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
|
data/test/controller_test.rb
CHANGED
@@ -4,22 +4,22 @@ require 'action_dispatch'
|
|
4
4
|
|
5
5
|
class ControllerTest < ActionController::TestCase
|
6
6
|
class Controller < ActionController::Base
|
7
|
-
|
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
|
14
|
-
post.
|
14
|
+
def write
|
15
|
+
post.update_attribute :message, params[:message]
|
15
16
|
|
16
17
|
render :json => post
|
17
18
|
end
|
18
19
|
|
19
|
-
|
20
|
-
|
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
|
-
|
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
|
55
|
-
assert_nothing_raised { get :
|
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
|
61
|
-
assert_nothing_raised { post :
|
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
|
67
|
-
assert_raises(ForgeryProtection::AttemptError) { get :
|
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
|
71
|
-
assert_raises(ForgeryProtection::AttemptError) { post :
|
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
|
93
|
+
def test_db_permitted_verification_write
|
75
94
|
before = Post.last.updated_at
|
76
|
-
assert_nothing_raised { get :
|
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.
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.7
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
8
|
-
|
9
|
-
autorequire:
|
7
|
+
authors:
|
8
|
+
- Dmitry Ratnikov
|
9
|
+
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
66
|
+
test_files:
|
67
|
+
- test/controller_test.rb
|
68
|
+
- test/db_events_test.rb
|
69
|
+
- test/test.sqlite3
|
70
|
+
- test/test_helper.rb
|
71
|
+
...
|