secure_headers 1.1.1 → 1.3.1

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.
Files changed (32) hide show
  1. checksums.yaml +15 -0
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +2 -0
  5. data/Gemfile +0 -4
  6. data/Guardfile +1 -5
  7. data/HISTORY.md +17 -0
  8. data/README.md +55 -26
  9. data/fixtures/rails_3_2_12/Gemfile +0 -2
  10. data/fixtures/rails_3_2_12/config/initializers/secure_headers.rb +1 -0
  11. data/fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb +10 -14
  12. data/fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb +7 -12
  13. data/fixtures/rails_3_2_12/spec/spec_helper.rb +1 -5
  14. data/fixtures/rails_3_2_12_no_init/Gemfile +0 -2
  15. data/fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb +7 -12
  16. data/fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb +7 -12
  17. data/fixtures/rails_3_2_12_no_init/spec/spec_helper.rb +3 -18
  18. data/lib/secure_headers/headers/content_security_policy.rb +46 -36
  19. data/lib/secure_headers/padrino.rb +14 -0
  20. data/lib/secure_headers/version.rb +1 -1
  21. data/spec/controllers/content_security_policy_controller_spec.rb +19 -19
  22. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +105 -85
  23. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +20 -20
  24. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +12 -12
  25. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +12 -12
  26. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +18 -18
  27. data/spec/lib/secure_headers_spec.rb +11 -11
  28. data/spec/spec_helper.rb +9 -24
  29. metadata +8 -12
  30. data/.rvmrc +0 -1
  31. data/fixtures/rails_3_2_12/Guardfile +0 -14
  32. data/fixtures/rails_3_2_12_no_init/Guardfile +0 -14
@@ -1,4 +1,5 @@
1
1
  require 'uri'
2
+ require 'base64'
2
3
 
3
4
  module SecureHeaders
4
5
  class ContentSecurityPolicyBuildError < StandardError; end
@@ -8,7 +9,7 @@ module SecureHeaders
8
9
  STANDARD_HEADER_NAME = "Content-Security-Policy"
9
10
  FF_CSP_ENDPOINT = "/content_security_policy/forward_report"
10
11
  DIRECTIVES = [:default_src, :script_src, :frame_src, :style_src, :img_src, :media_src, :font_src, :object_src, :connect_src]
11
- META = [:enforce, :http_additions, :disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
12
+ META = [:disable_chrome_extension, :disable_fill_missing, :forward_endpoint]
12
13
  end
13
14
  include Constants
14
15
 
@@ -30,6 +31,7 @@ module SecureHeaders
30
31
  def initialize(config=nil, options={})
31
32
  @experimental = !!options.delete(:experimental)
32
33
  @controller = options.delete(:controller)
34
+
33
35
  if options[:request]
34
36
  parse_request(options[:request])
35
37
  else
@@ -45,6 +47,10 @@ module SecureHeaders
45
47
  configure(config) if config
46
48
  end
47
49
 
50
+ def nonce
51
+ @nonce ||= SecureRandom.base64(32).chomp
52
+ end
53
+
48
54
  def configure(config)
49
55
  @config = config.dup
50
56
 
@@ -54,21 +60,24 @@ module SecureHeaders
54
60
  @config.merge!(experimental_config)
55
61
  end
56
62
 
63
+ # http_additions will be the only field that still doesn't support
64
+ # lambdas because it's an ugly api that's showing it's age.
65
+ @http_additions = @config.delete(:http_additions)
66
+
67
+ normalize_csp_options
68
+
57
69
  META.each do |meta|
58
70
  self.send("#{meta}=", @config.delete(meta))
59
71
  end
60
72
 
61
- @report_uri = @config.delete(:report_uri)
62
- @script_nonce = @config.delete(:script_nonce)
63
-
64
- normalize_csp_options
73
+ @enforce = @config.delete(:enforce)
65
74
  normalize_reporting_endpoint
66
75
  fill_directives unless disable_fill_missing?
67
76
  end
68
77
 
69
78
  def name
70
79
  base = STANDARD_HEADER_NAME
71
- if !enforce || experimental
80
+ if !@enforce || experimental
72
81
  base += "-Report-Only"
73
82
  end
74
83
  base
@@ -89,16 +98,9 @@ module SecureHeaders
89
98
  raise "Expected to find default_src directive value" unless @config[:default_src]
90
99
  append_http_additions unless ssl_request?
91
100
  header_value = [
92
- # ensure default-src is first
93
- build_directive(:default_src),
94
101
  generic_directives(@config),
95
- report_uri_directive,
96
- script_nonce_directive,
97
- ].join
98
-
99
- #store the value for next time
100
- @config = header_value
101
- header_value.strip
102
+ report_uri_directive
103
+ ].join.strip
102
104
  rescue StandardError => e
103
105
  raise ContentSecurityPolicyBuildError.new("Couldn't build CSP header :( #{e}")
104
106
  end
@@ -115,20 +117,31 @@ module SecureHeaders
115
117
  end
116
118
 
117
119
  def append_http_additions
118
- return unless http_additions
119
- http_additions.each do |k, v|
120
+ return unless @http_additions
121
+ @http_additions.each do |k, v|
120
122
  @config[k] ||= []
121
123
  @config[k] << v
122
124
  end
123
125
  end
124
126
 
125
127
  def normalize_csp_options
126
- @config.each do |k,v|
127
- @config[k] = v.split if v.is_a? String
128
- @config[k] = @config[k].map do |val|
129
- translate_dir_value(val)
128
+ @config = @config.inject({}) do |hash, (key, value)|
129
+ # lambdas
130
+ config_val = value.respond_to?(:call) ? value.call : value
131
+ # space-delimeted strings
132
+ config_val = config_val.split if config_val.is_a? String
133
+ # array of strings
134
+ if config_val.respond_to?(:map) #skip booleans
135
+ config_val = config_val.map do |val|
136
+ translate_dir_value(val)
137
+ end.flatten.uniq
130
138
  end
139
+
140
+ hash[key] = config_val
141
+ hash
131
142
  end
143
+
144
+ @report_uri = @config.delete(:report_uri).join(" ") if @config[:report_uri]
132
145
  end
133
146
 
134
147
  # translates 'inline','self', 'none' and 'eval' to their respective impl-specific values.
@@ -138,6 +151,9 @@ module SecureHeaders
138
151
  # self/none are special sources/src-dir-values and need to be quoted in chrome
139
152
  elsif %{self none}.include?(val)
140
153
  "'#{val}'"
154
+ elsif val == 'nonce'
155
+ @controller.instance_variable_set(:@content_security_policy_nonce, nonce)
156
+ ["'nonce-#{nonce}'", "'unsafe-inline'"]
141
157
  else
142
158
  val
143
159
  end
@@ -161,8 +177,13 @@ module SecureHeaders
161
177
  def same_origin?
162
178
  return unless report_uri && request_uri
163
179
 
164
- origin = URI.parse(request_uri)
165
- uri = URI.parse(report_uri)
180
+ begin
181
+ origin = URI.parse(request_uri)
182
+ uri = URI.parse(report_uri)
183
+ rescue URI::InvalidURIError
184
+ return false
185
+ end
186
+
166
187
  uri.host == origin.host && origin.port == uri.port && origin.scheme == uri.scheme
167
188
  end
168
189
 
@@ -180,26 +201,15 @@ module SecureHeaders
180
201
  "report-uri #{@report_uri};"
181
202
  end
182
203
 
183
- def script_nonce_directive
184
- return '' if @script_nonce.nil?
185
- nonce_value = if @script_nonce.is_a?(String)
186
- @script_nonce
187
- elsif @controller
188
- @controller.instance_exec(&@script_nonce)
189
- else
190
- @script_nonce.call
191
- end
192
- "script-nonce #{nonce_value};"
193
- end
194
-
195
204
  def generic_directives(config)
196
205
  header_value = ''
197
206
  if config[:img_src]
198
207
  config[:img_src] = config[:img_src] + ['data:'] unless config[:img_src].include?('data:')
199
208
  else
200
- config[:img_src] = ['data:']
209
+ config[:img_src] = config[:default_src] + ['data:']
201
210
  end
202
211
 
212
+ header_value = build_directive(:default_src)
203
213
  config.keys.sort_by{|k| k.to_s}.each do |k| # ensure consistent ordering
204
214
  header_value += build_directive(k)
205
215
  end
@@ -0,0 +1,14 @@
1
+ module SecureHeaders
2
+ module Padrino
3
+ class << self
4
+ ##
5
+ # Main class that register this extension.
6
+ #
7
+ def registered(app)
8
+ app.extend SecureHeaders::ClassMethods
9
+ app.helpers SecureHeaders::InstanceMethods
10
+ end
11
+ alias :included :registered
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,3 @@
1
1
  module SecureHeaders
2
- VERSION = "1.1.1"
2
+ VERSION = "1.3.1"
3
3
  end
@@ -32,58 +32,58 @@ describe ContentSecurityPolicyController do
32
32
  let(:secondary_endpoint) { "https://internal.example.com" }
33
33
 
34
34
  before(:each) do
35
- SecureHeaders::Configuration.stub(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
36
- subject.should_receive :head
37
- subject.stub(:params).and_return(params)
38
- subject.stub(:request).and_return(FakeRequest.new)
39
- Net::HTTP.any_instance.stub(:request)
35
+ allow(SecureHeaders::Configuration).to receive(:csp).and_return({:report_uri => endpoint, :forward_endpoint => secondary_endpoint})
36
+ expect(subject).to receive :head
37
+ allow(subject).to receive(:params).and_return(params)
38
+ allow(subject).to receive(:request).and_return(FakeRequest.new)
39
+ allow_any_instance_of(Net::HTTP).to receive(:request)
40
40
  end
41
41
 
42
42
  context "delivery endpoint" do
43
43
  it "posts over ssl" do
44
- subject.should_receive(:use_ssl)
44
+ expect(subject).to receive(:use_ssl)
45
45
  subject.scribe
46
46
  end
47
47
 
48
48
  it "posts over plain http" do
49
- SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => 'http://example.com')
50
- subject.should_not_receive(:use_ssl)
49
+ allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => 'http://example.com')
50
+ expect(subject).not_to receive(:use_ssl)
51
51
  subject.scribe
52
52
  end
53
53
  end
54
54
 
55
55
  it "makes a POST request" do
56
- Net::HTTP.stub(:new).and_return(request)
57
- request.should_receive(:request).with(instance_of(::Net::HTTP::Post))
58
- params.stub(:to_json)
56
+ allow(Net::HTTP).to receive(:new).and_return(request)
57
+ expect(request).to receive(:request).with(instance_of(::Net::HTTP::Post))
58
+ allow(params).to receive(:to_json)
59
59
  subject.scribe
60
60
  end
61
61
 
62
62
  it "POSTs to the configured forward_endpoint" do
63
- Net::HTTP::Post.should_receive(:new).with(secondary_endpoint).and_return(request)
63
+ expect(Net::HTTP::Post).to receive(:new).with(secondary_endpoint).and_return(request)
64
64
  subject.scribe
65
65
  end
66
66
 
67
67
  it "does not POST if there is no forwarder configured" do
68
- SecureHeaders::Configuration.stub(:csp).and_return({})
69
- Net::HTTP::Post.should_not_receive(:new)
68
+ allow(SecureHeaders::Configuration).to receive(:csp).and_return({})
69
+ expect(Net::HTTP::Post).not_to receive(:new)
70
70
  subject.scribe
71
71
  end
72
72
 
73
73
  it "eliminates known phony CSP reports" do
74
- SecureHeaders::Configuration.stub(:csp).and_return(:report_uri => nil)
75
- Net::HTTP::Post.should_not_receive :new
74
+ allow(SecureHeaders::Configuration).to receive(:csp).and_return(:report_uri => nil)
75
+ expect(Net::HTTP::Post).not_to receive :new
76
76
  subject.scribe
77
77
  end
78
78
 
79
79
  it "logs errors when it cannot forward the CSP report" do
80
80
  class Rails; def logger; end; end
81
81
  logger = double(:repond_to? => true)
82
- Rails.stub(:logger).and_return(logger)
82
+ allow(Rails).to receive(:logger).and_return(logger)
83
83
 
84
- SecureHeaders::Configuration.stub(:csp).and_raise(StandardError)
84
+ allow(SecureHeaders::Configuration).to receive(:csp).and_raise(StandardError)
85
85
 
86
- logger.should_receive(:warn)
86
+ expect(logger).to receive(:warn)
87
87
  subject.scribe
88
88
  end
89
89
  end