Skip to content

Commit 46764fa

Browse files
committed
wip
1 parent b5fe9d4 commit 46764fa

File tree

4 files changed

+95
-51
lines changed

4 files changed

+95
-51
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
33
gemspec
44

55
group :test do
6+
gem 'pry'
67
gem 'test-unit', '~> 3.0'
78
gem 'rails', '3.2.22'
89
gem 'sqlite3', :platforms => [:ruby, :mswin, :mingw]

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ The following methods are going to be called, unless they are provided in a `ski
6161
:form_action => "'self' github.com",
6262
:frame_ancestors => "'none'",
6363
:plugin_types => 'application/x-shockwave-flash',
64+
:block_all_mixed_content => '' # see [http://www.w3.org/TR/mixed-content/]()
6465
:report_uri => '//example.com/uri-directive'
6566
}
6667
config.hpkp = {
@@ -99,7 +100,7 @@ Sometimes you need to override your content security policy for a given endpoint
99100
1. Override the `secure_header_options_for` class instance method. e.g.
100101

101102
```ruby
102-
class SomethingController < ApplicationController
103+
class SomethingController < ApplicationController
103104
def wumbus
104105
# gets style-src override
105106
end

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 87 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'securerandom'
44
require 'user_agent_parser'
55
require 'json'
6+
require 'pry'
67

78
module SecureHeaders
89
class ContentSecurityPolicyBuildError < StandardError; end
@@ -11,28 +12,62 @@ module Constants
1112
DEFAULT_CSP_HEADER = "default-src https: data: 'unsafe-inline' 'unsafe-eval'; frame-src https: about: javascript:; img-src data:"
1213
HEADER_NAME = "Content-Security-Policy"
1314
ENV_KEY = 'secure_headers.content_security_policy'
14-
DIRECTIVES = [
15+
16+
DIRECTIVES_1_0 = [
1517
:default_src,
1618
:connect_src,
1719
:font_src,
1820
:frame_src,
1921
:img_src,
2022
:media_src,
2123
:object_src,
24+
:sandbox,
2225
:script_src,
2326
:style_src,
24-
:base_uri,
27+
:report_uri
28+
].freeze
29+
30+
DIRECTIVES_2_0 = [
31+
DIRECTIVES_1_0,
32+
:base_url,
2533
:child_src,
2634
:form_action,
2735
:frame_ancestors,
2836
:plugin_types
29-
]
37+
].flatten.freeze
3038

31-
OTHER = [
32-
:report_uri
33-
]
3439

35-
ALL_DIRECTIVES = DIRECTIVES + OTHER
40+
# All the directives currently under consideration for CSP level 3.
41+
# https://w3c.github.io/webappsec/specs/CSP2/
42+
DIRECTIVES_3_0 = [
43+
DIRECTIVES_2_0,
44+
:manifest_src,
45+
:reflected_xss
46+
].flatten.freeze
47+
48+
# All the directives that are not currently in a formal spec, but have
49+
# been implemented somewhere.
50+
DIRECTIVES_DRAFT = [
51+
:block_all_mixed_content,
52+
].freeze
53+
54+
SAFARI_DIRECTIVES = DIRECTIVES_1_0
55+
56+
FIREFOX_UNSUPPORTED_DIRECTIVES = [
57+
:block_all_mixed_content,
58+
:child_src,
59+
:plugin_types
60+
].freeze
61+
62+
FIREFOX_DIRECTIVES = (
63+
DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
64+
).freeze
65+
66+
CHROME_DIRECTIVES = (
67+
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
68+
).freeze
69+
70+
ALL_DIRECTIVES = DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT
3671
CONFIG_KEY = :csp
3772
end
3873

@@ -99,33 +134,36 @@ def initialize(config=nil, options={})
99134
@ua = options[:ua]
100135
@ssl_request = !!options.delete(:ssl)
101136
@request_uri = options.delete(:request_uri)
137+
@http_additions = config.delete(:http_additions)
138+
@app_name = config.delete(:app_name)
139+
@enforce = !!config.delete(:enforce)
140+
@disable_img_src_data_uri = !!config.delete(:disable_img_src_data_uri)
141+
@tag_report_uri = !!config.delete(:tag_report_uri)
142+
@script_hashes = config.delete(:script_hashes) || []
102143

103144
# Config values can be string, array, or lamdba values
104145
@config = config.inject({}) do |hash, (key, value)|
105146
config_val = value.respond_to?(:call) ? value.call(@controller) : value
106-
107-
if DIRECTIVES.include?(key) # directives need to be normalized to arrays of strings
147+
if ContentSecurityPolicy::ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings
108148
config_val = config_val.split if config_val.is_a? String
109149
if config_val.is_a?(Array)
110150
config_val = config_val.map do |val|
111151
translate_dir_value(val)
112152
end.flatten.uniq
113153
end
154+
else
155+
raise ArgumentError.new("Unknown directive supplied: #{key}")
114156
end
115157

158+
116159
hash[key] = config_val
117160
hash
118161
end
119162

120-
@http_additions = @config.delete(:http_additions)
121-
@app_name = @config.delete(:app_name)
122-
@report_uri = @config.delete(:report_uri)
123-
@enforce = !!@config.delete(:enforce)
124-
@disable_img_src_data_uri = !!@config.delete(:disable_img_src_data_uri)
125-
@tag_report_uri = !!@config.delete(:tag_report_uri)
126-
@script_hashes = @config.delete(:script_hashes) || []
127-
128163
add_script_hashes if @script_hashes.any?
164+
puts @config
165+
strip_unsupported_directives
166+
puts @config
129167
end
130168

131169
##
@@ -160,13 +198,20 @@ def value
160198

161199
def to_json
162200
build_value
163-
@config.to_json.gsub(/(\w+)_src/, "\\1-src")
201+
out = @config.inject({}) do |hash, (key, value)|
202+
hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value
203+
hash
204+
end
205+
puts out
206+
out.to_json
164207
end
165208

166209
def self.from_json(*json_configs)
167210
json_configs.inject({}) do |combined_config, one_config|
168-
one_config = one_config.gsub(/(\w+)-src/, "\\1_src")
169-
config = JSON.parse(one_config, :symbolize_names => true)
211+
config = JSON.parse(one_config).inject({}) do |hash, (key, value)|
212+
hash[key.gsub(/(\w+)-(\w+)/, "\\1_\\2").to_sym] = value
213+
hash
214+
end
170215
combined_config.merge(config) do |_, lhs, rhs|
171216
lhs | rhs
172217
end
@@ -182,10 +227,7 @@ def add_script_hashes
182227
def build_value
183228
raise "Expected to find default_src directive value" unless @config[:default_src]
184229
append_http_additions unless ssl_request?
185-
header_value = [
186-
generic_directives,
187-
report_uri_directive
188-
].join.strip
230+
generic_directives
189231
end
190232

191233
def append_http_additions
@@ -204,7 +246,7 @@ def translate_dir_value val
204246
warn "[DEPRECATION] using self/none may not be supported in the future. Instead use 'self'/'none' instead."
205247
"'#{val}'"
206248
elsif val == 'nonce'
207-
if supports_nonces?(@ua)
249+
if supports_nonces?
208250
self.class.set_nonce(@controller, nonce)
209251
["'nonce-#{nonce}'", "'unsafe-inline'"]
210252
else
@@ -215,25 +257,6 @@ def translate_dir_value val
215257
end
216258
end
217259

218-
def report_uri_directive
219-
return '' if @report_uri.nil?
220-
221-
if @report_uri.start_with?('//')
222-
@report_uri = if @ssl_request
223-
"https:" + @report_uri
224-
else
225-
"http:" + @report_uri
226-
end
227-
end
228-
229-
if @tag_report_uri
230-
@report_uri = "#{@report_uri}?enforce=#{@enforce}"
231-
@report_uri += "&app_name=#{@app_name}" if @app_name
232-
end
233-
234-
"report-uri #{@report_uri};"
235-
end
236-
237260
def generic_directives
238261
header_value = ''
239262
data_uri = @disable_img_src_data_uri ? [] : ["data:"]
@@ -243,7 +266,7 @@ def generic_directives
243266
@config[:img_src] = @config[:default_src] + data_uri
244267
end
245268

246-
DIRECTIVES.each do |directive_name|
269+
ALL_DIRECTIVES.each do |directive_name|
247270
header_value += build_directive(directive_name) if @config[directive_name]
248271
end
249272

@@ -254,8 +277,25 @@ def build_directive(key)
254277
"#{self.class.symbol_to_hyphen_case(key)} #{@config[key].join(" ")}; "
255278
end
256279

257-
def supports_nonces?(user_agent)
258-
parsed_ua = UserAgentParser.parse(user_agent)
280+
def strip_unsupported_directives
281+
@config.select! { |key, _| supported_directives.include?(key) }
282+
end
283+
284+
def supported_directives
285+
@supported_directives ||= case UserAgentParser.parse(@ua).family
286+
when "Chrome"
287+
CHROME_DIRECTIVES
288+
when "Safari"
289+
SAFARI_DIRECTIVES
290+
when "Firefox"
291+
FIREFOX_DIRECTIVES
292+
else
293+
DIRECTIVES_1_0
294+
end
295+
end
296+
297+
def supports_nonces?
298+
parsed_ua = UserAgentParser.parse(@ua)
259299
["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
260300
end
261301
end

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ module SecureHeaders
55
let(:default_opts) do
66
{
77
:default_src => 'https:',
8-
:report_uri => '/csp_report',
8+
:img_src => "https: data:",
99
:script_src => "'unsafe-inline' 'unsafe-eval' https: data:",
10-
:style_src => "'unsafe-inline' https: about:"
10+
:style_src => "'unsafe-inline' https: about:",
11+
:report_uri => '/csp_report'
1112
}
1213
end
1314
let(:controller) { DummyClass.new }
@@ -58,7 +59,8 @@ def request_for user_agent, request_uri=nil, options={:ssl => false}
5859

5960
it "exports a policy to JSON" do
6061
policy = ContentSecurityPolicy.new(default_opts)
61-
expected = %({"default-src":["https:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"img-src":["https:","data:"]})
62+
puts default_opts
63+
expected = %({"default-src":["https:"],"img-src":["https:","data:"],"script-src":["'unsafe-inline'","'unsafe-eval'","https:","data:"],"style-src":["'unsafe-inline'","https:","about:"],"report-uri":["/csp_report"]})
6264
expect(policy.to_json).to eq(expected)
6365
end
6466

0 commit comments

Comments
 (0)