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.
- data/lib/forgery_protection/controller_extension.rb +31 -0
- data/lib/forgery_protection/instrumenter_extension.rb +14 -0
- data/lib/forgery_protection/query_tracker.rb +22 -0
- data/lib/forgery_protection/sql_event.rb +23 -0
- data/lib/forgery_protection/version.rb +3 -0
- data/lib/strict-forgery-protection.rb +11 -0
- data/test/controller_test.rb +80 -0
- data/test/db_events_test.rb +70 -0
- data/test/test.sqlite3 +0 -0
- data/test/test_helper.rb +30 -0
- metadata +77 -0
@@ -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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|