rack-padlock 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +36 -0
- data/lib/rack/padlock/version.rb +5 -0
- data/lib/rack/padlock.rb +48 -0
- data/lib/rack-padlock.rb +4 -0
- data/lib/tasks/rack-padlock.rake +23 -0
- data/test/rack_padlock_test.rb +70 -0
- data/test/test_helper.rb +24 -0
- metadata +120 -0
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
rack-padlock
|
2
|
+
=======
|
3
|
+
|
4
|
+
A toolkit for writing tests against rack applications that ensure all traffic on a page is secure.
|
5
|
+
|
6
|
+
###Prerequisites
|
7
|
+
|
8
|
+
1. You need to have a rack based application
|
9
|
+
2. The application must have a browser based integration test suite. I recommend capybara.
|
10
|
+
3. Your integration tests must use HTTPS
|
11
|
+
|
12
|
+
## Setup
|
13
|
+
|
14
|
+
Add rack-padlock gem to your test group
|
15
|
+
|
16
|
+
```bash
|
17
|
+
group :test do
|
18
|
+
gem 'rack-padlock'
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
Add rack-padlock middleware to your app
|
23
|
+
|
24
|
+
```bash
|
25
|
+
group :test do
|
26
|
+
gem 'rack-padlock'
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
Please see the example application at https://github.com/joshuacronemeyer/rack-padlock-example-app
|
31
|
+
|
32
|
+
how it works
|
33
|
+
=======
|
34
|
+
|
35
|
+
|
36
|
+
Please see https://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html
|
data/lib/rack/padlock.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Rack
|
2
|
+
class Padlock
|
3
|
+
POST_BODY = 'rack.input'.freeze
|
4
|
+
|
5
|
+
def initialize(app, options={})
|
6
|
+
default_options = {
|
7
|
+
:log_file => "tmp/padlock.log"
|
8
|
+
}
|
9
|
+
@app = app
|
10
|
+
@options = default_options.merge(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
return capture_violation(env) if csp_policy_violation?(env)
|
15
|
+
status, headers, body = @app.call(env)
|
16
|
+
headers.merge!(csp_headers(env))
|
17
|
+
[status, headers, body]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def capture_violation(env)
|
23
|
+
violation = env[POST_BODY].read
|
24
|
+
PadlockFile.write(@options[:log_file], violation)
|
25
|
+
[200, {}, []]
|
26
|
+
end
|
27
|
+
|
28
|
+
def csp_policy_violation?(env)
|
29
|
+
env['PATH_INFO'] =~ /padlock_middleware\/report$/
|
30
|
+
end
|
31
|
+
|
32
|
+
def csp_headers(env)
|
33
|
+
host = env["HTTP_HOST"]
|
34
|
+
report_uri = "#{host}/padlock_middleware/report"
|
35
|
+
csp_header_names = %w(Content-Security-Policy-Report-Only X-Content-Security-Policy-Report-Only X-WebKit-CSP-Report-Only)
|
36
|
+
csp_headers = {}
|
37
|
+
csp_header_names.each{|name| csp_headers[name] = "default-src https: 'unsafe-inline' 'unsafe-eval'; report-uri http://#{report_uri}"}
|
38
|
+
csp_headers
|
39
|
+
end
|
40
|
+
|
41
|
+
class PadlockFile
|
42
|
+
def self.write(file, violation)
|
43
|
+
::File.open(file, 'a+') { |file| file.puts violation.strip }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/rack-padlock.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
PADLOCK_LOG = 'tmp/padlock.log'
|
3
|
+
|
4
|
+
desc "Test for padlock"
|
5
|
+
task :padlock_test => [:padlock_clean, :test] do
|
6
|
+
contents = File.open(PADLOCK_LOG, 'r') { |f| f.readlines }
|
7
|
+
if File.size? PADLOCK_LOG
|
8
|
+
puts "Padlock test failure: Insecure content is being loaded on the page.".foreground(:red)
|
9
|
+
contents.each do |line|
|
10
|
+
blocked_uri = line.match(/blocked-uri":"([^"]+)/)[1].foreground(:yellow)
|
11
|
+
violated_directive = line.match(/violated-directive":"([^"]+)/)[1].foreground(:magenta)
|
12
|
+
puts "Request for #{blocked_uri} has violated directive #{violated_directive}"
|
13
|
+
end
|
14
|
+
exit 1
|
15
|
+
else
|
16
|
+
puts "Padlock test success: page is secure.".foreground(:green)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Clean padlock logs"
|
21
|
+
task :padlock_clean do
|
22
|
+
File.truncate('tmp/padlock.log', 0) if File.exist?(PADLOCK_LOG)
|
23
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'test_helper')
|
2
|
+
|
3
|
+
class RackPadlockTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
describe "Padlock middleware headers" do
|
6
|
+
before do
|
7
|
+
mock_app
|
8
|
+
end
|
9
|
+
|
10
|
+
it "sets CSP report only" do
|
11
|
+
get 'http://oregonhero.com/wagons/1'
|
12
|
+
last_response.headers['Content-Security-Policy-Report-Only'].wont_be_nil
|
13
|
+
last_response.headers['X-Content-Security-Policy-Report-Only'].wont_be_nil
|
14
|
+
last_response.headers['X-WebKit-CSP-Report-Only'].wont_be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it "sets the report-uri to a value that will be intercepted by padlock middleware" do
|
18
|
+
get 'http://oregonhero.com/wagons/1', {}, 'HTTP_HOST' => "http://oregonhero.com"
|
19
|
+
last_response.headers['Content-Security-Policy-Report-Only'].must_match "http://oregonhero.com/padlock_middleware/report"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "Padlock captures policy violation reports" do
|
24
|
+
before do
|
25
|
+
mock_app
|
26
|
+
end
|
27
|
+
|
28
|
+
it "prevents the policy violations from going to the app" do
|
29
|
+
post 'http://oregonhero.com/padlock_middleware/report' do
|
30
|
+
"Knock, Knock"
|
31
|
+
end
|
32
|
+
last_response.status.must_equal 200
|
33
|
+
last_response.body.must_equal ""
|
34
|
+
end
|
35
|
+
|
36
|
+
it "doesn't interfere with other traffic" do
|
37
|
+
get 'http://oregonhero.com/wagons/1'
|
38
|
+
last_response.body.must_equal "Hello world!"
|
39
|
+
end
|
40
|
+
|
41
|
+
# TODO figure out how to set the post data so we can test we log it.
|
42
|
+
# it "logs policy violations to padlock logfile" do
|
43
|
+
# post 'http://oregonhero.com/padlock_middleware/report', {}, {"RAW_POST_DATA" => "Knock"}
|
44
|
+
# last_response.status.must_equal 200
|
45
|
+
# last_response.body.must_equal ""
|
46
|
+
# Rack::Padlock::PadlockFile.last_output.must_match /Knock/
|
47
|
+
# end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Rack::Padlock::PadlockFile
|
51
|
+
@@last_output = ""
|
52
|
+
def self.write(data)
|
53
|
+
@@last_output = data
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.last_output
|
57
|
+
@@last_output
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.last_output=(data)
|
61
|
+
@@last_output = data
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class DummyApp
|
66
|
+
def call(env)
|
67
|
+
return [{}, {}, {}]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'rack/mock'
|
3
|
+
require 'rack/test'
|
4
|
+
require 'rack/padlock'
|
5
|
+
|
6
|
+
class MiniTest::Unit::TestCase
|
7
|
+
include Rack::Test::Methods
|
8
|
+
|
9
|
+
def app; Rack::Lint.new(@app); end
|
10
|
+
|
11
|
+
def mock_app(options = {})
|
12
|
+
main_app = lambda { |env|
|
13
|
+
request = Rack::Request.new(env)
|
14
|
+
headers = {'Content-Type' => "text/html"}
|
15
|
+
[200, headers, ['Hello world!']]
|
16
|
+
}
|
17
|
+
|
18
|
+
builder = Rack::Builder.new
|
19
|
+
builder.use Rack::Padlock, options
|
20
|
+
builder.run main_app
|
21
|
+
@app = builder.to_app
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-padlock
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Josh Cronemeyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-07 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
16
|
+
requirement: &70300117186540 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70300117186540
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rainbow
|
27
|
+
requirement: &70300117186000 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70300117186000
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: bundler
|
38
|
+
requirement: &70300117185120 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70300117185120
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70300117183780 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70300117183780
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: minitest
|
60
|
+
requirement: &70300117183120 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70300117183120
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: &70300117182600 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70300117182600
|
80
|
+
description: A Gem for testing web applications don't generate mixed secure/insecure
|
81
|
+
traffic. Keep that browser padlock locked!
|
82
|
+
email: joshuacronemeyer@gmail.com
|
83
|
+
executables: []
|
84
|
+
extensions: []
|
85
|
+
extra_rdoc_files: []
|
86
|
+
files:
|
87
|
+
- lib/rack/padlock/version.rb
|
88
|
+
- lib/rack/padlock.rb
|
89
|
+
- lib/rack-padlock.rb
|
90
|
+
- lib/tasks/rack-padlock.rake
|
91
|
+
- README.md
|
92
|
+
- test/rack_padlock_test.rb
|
93
|
+
- test/test_helper.rb
|
94
|
+
homepage: https://github.com/joshuacronemeyer/rack-padlock
|
95
|
+
licenses: []
|
96
|
+
post_install_message:
|
97
|
+
rdoc_options: []
|
98
|
+
require_paths:
|
99
|
+
- lib
|
100
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ! '>='
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 1.8.10
|
115
|
+
signing_key:
|
116
|
+
specification_version: 3
|
117
|
+
summary: A Toolkit for writing tests that ensure all traffic on a page is secure.
|
118
|
+
test_files:
|
119
|
+
- test/rack_padlock_test.rb
|
120
|
+
- test/test_helper.rb
|