secure_headers 3.4.0 → 3.6.0
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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +75 -0
- data/Gemfile +1 -0
- data/README.md +54 -322
- data/docs/HPKP.md +17 -0
- data/docs/cookies.md +50 -0
- data/docs/hashes.md +64 -0
- data/docs/named_overrides_and_appends.md +107 -0
- data/docs/per_action_configuration.md +105 -0
- data/docs/sinatra.md +25 -0
- data/lib/secure_headers/configuration.rb +71 -37
- data/lib/secure_headers/headers/clear_site_data.rb +51 -0
- data/lib/secure_headers/headers/content_security_policy.rb +64 -37
- data/lib/secure_headers/headers/content_security_policy_config.rb +128 -0
- data/lib/secure_headers/headers/policy_management.rb +49 -30
- data/lib/secure_headers/headers/referrer_policy.rb +3 -0
- data/lib/secure_headers/view_helper.rb +8 -0
- data/lib/secure_headers.rb +133 -55
- data/secure_headers.gemspec +1 -1
- data/spec/lib/secure_headers/configuration_spec.rb +5 -5
- data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +103 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +14 -2
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +25 -34
- data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +18 -0
- data/spec/lib/secure_headers/middleware_spec.rb +13 -1
- data/spec/lib/secure_headers/view_helpers_spec.rb +19 -6
- data/spec/lib/secure_headers_spec.rb +289 -40
- data/spec/spec_helper.rb +3 -2
- metadata +12 -2
|
@@ -1,37 +1,45 @@
|
|
|
1
1
|
require_relative 'policy_management'
|
|
2
|
+
require_relative 'content_security_policy_config'
|
|
2
3
|
require 'useragent'
|
|
3
4
|
|
|
4
5
|
module SecureHeaders
|
|
5
|
-
class ContentSecurityPolicyConfigError < StandardError; end
|
|
6
6
|
class ContentSecurityPolicy
|
|
7
7
|
include PolicyManagement
|
|
8
8
|
|
|
9
9
|
# constants to be used for version-specific UA sniffing
|
|
10
10
|
VERSION_46 = ::UserAgent::Version.new("46")
|
|
11
|
+
VERSION_10 = ::UserAgent::Version.new("10")
|
|
12
|
+
FALLBACK_VERSION = ::UserAgent::Version.new("0")
|
|
11
13
|
|
|
12
14
|
def initialize(config = nil, user_agent = OTHER)
|
|
13
|
-
@config =
|
|
15
|
+
@config = if config.is_a?(Hash)
|
|
16
|
+
if config[:report_only]
|
|
17
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
|
18
|
+
else
|
|
19
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
|
20
|
+
end
|
|
21
|
+
elsif config.nil?
|
|
22
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
|
23
|
+
else
|
|
24
|
+
config
|
|
25
|
+
end
|
|
26
|
+
|
|
14
27
|
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
15
28
|
user_agent
|
|
16
29
|
else
|
|
17
30
|
UserAgent.parse(user_agent)
|
|
18
31
|
end
|
|
19
|
-
normalize_child_frame_src
|
|
20
|
-
@
|
|
21
|
-
@
|
|
22
|
-
@
|
|
23
|
-
@style_nonce = @config[:style_nonce]
|
|
32
|
+
@frame_src = normalize_child_frame_src
|
|
33
|
+
@preserve_schemes = @config.preserve_schemes
|
|
34
|
+
@script_nonce = @config.script_nonce
|
|
35
|
+
@style_nonce = @config.style_nonce
|
|
24
36
|
end
|
|
25
37
|
|
|
26
38
|
##
|
|
27
39
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
28
40
|
# "Content-Security-Policy-Report-Only"
|
|
29
41
|
def name
|
|
30
|
-
|
|
31
|
-
REPORT_ONLY
|
|
32
|
-
else
|
|
33
|
-
HEADER_NAME
|
|
34
|
-
end
|
|
42
|
+
@config.class.const_get(:HEADER_NAME)
|
|
35
43
|
end
|
|
36
44
|
|
|
37
45
|
##
|
|
@@ -49,16 +57,16 @@ module SecureHeaders
|
|
|
49
57
|
# frame-src is deprecated, child-src is being implemented. They are
|
|
50
58
|
# very similar and in most cases, the same value can be used for both.
|
|
51
59
|
def normalize_child_frame_src
|
|
52
|
-
if @config
|
|
60
|
+
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
|
|
53
61
|
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.")
|
|
54
|
-
elsif @config
|
|
55
|
-
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config
|
|
62
|
+
elsif @config.frame_src
|
|
63
|
+
Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] :frame_src is deprecated, use :child_src instead. Provided: #{@config.frame_src}.")
|
|
56
64
|
end
|
|
57
65
|
|
|
58
66
|
if supported_directives.include?(:child_src)
|
|
59
|
-
@config
|
|
67
|
+
@config.child_src || @config.frame_src
|
|
60
68
|
else
|
|
61
|
-
@config
|
|
69
|
+
@config.frame_src || @config.child_src
|
|
62
70
|
end
|
|
63
71
|
end
|
|
64
72
|
|
|
@@ -73,9 +81,9 @@ module SecureHeaders
|
|
|
73
81
|
directives.map do |directive_name|
|
|
74
82
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
75
83
|
when :boolean
|
|
76
|
-
symbol_to_hyphen_case(directive_name) if @config
|
|
84
|
+
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
|
77
85
|
when :string
|
|
78
|
-
[symbol_to_hyphen_case(directive_name), @config
|
|
86
|
+
[symbol_to_hyphen_case(directive_name), @config.directive_value(directive_name)].join(" ")
|
|
79
87
|
else
|
|
80
88
|
build_directive(directive_name)
|
|
81
89
|
end
|
|
@@ -88,11 +96,19 @@ module SecureHeaders
|
|
|
88
96
|
#
|
|
89
97
|
# Returns a string representing a directive.
|
|
90
98
|
def build_directive(directive)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
99
|
+
source_list = case directive
|
|
100
|
+
when :child_src
|
|
101
|
+
if supported_directives.include?(:child_src)
|
|
102
|
+
@frame_src
|
|
103
|
+
end
|
|
104
|
+
when :frame_src
|
|
105
|
+
unless supported_directives.include?(:child_src)
|
|
106
|
+
@frame_src
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
@config.directive_value(directive)
|
|
110
|
+
end
|
|
111
|
+
return unless source_list && source_list.any?
|
|
96
112
|
normalized_source_list = minify_source_list(directive, source_list)
|
|
97
113
|
[symbol_to_hyphen_case(directive), normalized_source_list].join(" ")
|
|
98
114
|
end
|
|
@@ -101,16 +117,17 @@ module SecureHeaders
|
|
|
101
117
|
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
102
118
|
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
103
119
|
def minify_source_list(directive, source_list)
|
|
120
|
+
source_list = source_list.compact
|
|
104
121
|
if source_list.include?(STAR)
|
|
105
122
|
keep_wildcard_sources(source_list)
|
|
106
123
|
else
|
|
107
|
-
populate_nonces
|
|
108
|
-
reject_all_values_if_none
|
|
124
|
+
source_list = populate_nonces(directive, source_list)
|
|
125
|
+
source_list = reject_all_values_if_none(source_list)
|
|
109
126
|
|
|
110
127
|
unless directive == REPORT_URI || @preserve_schemes
|
|
111
|
-
strip_source_schemes
|
|
128
|
+
source_list = strip_source_schemes(source_list)
|
|
112
129
|
end
|
|
113
|
-
dedup_source_list(source_list)
|
|
130
|
+
dedup_source_list(source_list)
|
|
114
131
|
end
|
|
115
132
|
end
|
|
116
133
|
|
|
@@ -120,8 +137,12 @@ module SecureHeaders
|
|
|
120
137
|
end
|
|
121
138
|
|
|
122
139
|
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
123
|
-
def reject_all_values_if_none
|
|
124
|
-
|
|
140
|
+
def reject_all_values_if_none(source_list)
|
|
141
|
+
if source_list.length > 1
|
|
142
|
+
source_list.reject { |value| value == NONE }
|
|
143
|
+
else
|
|
144
|
+
source_list
|
|
145
|
+
end
|
|
125
146
|
end
|
|
126
147
|
|
|
127
148
|
# Removes duplicates and sources that already match an existing wild card.
|
|
@@ -143,12 +164,14 @@ module SecureHeaders
|
|
|
143
164
|
|
|
144
165
|
# Private: append a nonce to the script/style directories if script_nonce
|
|
145
166
|
# or style_nonce are provided.
|
|
146
|
-
def populate_nonces
|
|
167
|
+
def populate_nonces(directive, source_list)
|
|
147
168
|
case directive
|
|
148
169
|
when SCRIPT_SRC
|
|
149
170
|
append_nonce(source_list, @script_nonce)
|
|
150
171
|
when STYLE_SRC
|
|
151
172
|
append_nonce(source_list, @style_nonce)
|
|
173
|
+
else
|
|
174
|
+
source_list
|
|
152
175
|
end
|
|
153
176
|
end
|
|
154
177
|
|
|
@@ -165,19 +188,23 @@ module SecureHeaders
|
|
|
165
188
|
source_list << UNSAFE_INLINE
|
|
166
189
|
end
|
|
167
190
|
end
|
|
191
|
+
|
|
192
|
+
source_list
|
|
168
193
|
end
|
|
169
194
|
|
|
170
195
|
# Private: return the list of directives that are supported by the user agent,
|
|
171
196
|
# starting with default-src and ending with report-uri.
|
|
172
197
|
def directives
|
|
173
|
-
[
|
|
198
|
+
[
|
|
199
|
+
DEFAULT_SRC,
|
|
174
200
|
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
175
|
-
REPORT_URI
|
|
201
|
+
REPORT_URI
|
|
202
|
+
].flatten
|
|
176
203
|
end
|
|
177
204
|
|
|
178
205
|
# Private: Remove scheme from source expressions.
|
|
179
|
-
def strip_source_schemes
|
|
180
|
-
source_list.map
|
|
206
|
+
def strip_source_schemes(source_list)
|
|
207
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
181
208
|
end
|
|
182
209
|
|
|
183
210
|
# Private: determine which directives are supported for the given user agent.
|
|
@@ -187,7 +214,7 @@ module SecureHeaders
|
|
|
187
214
|
# Returns an array of symbols representing the directives.
|
|
188
215
|
def supported_directives
|
|
189
216
|
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
|
|
190
|
-
if @parsed_ua.browser == "Firefox" && @parsed_ua.version >= VERSION_46
|
|
217
|
+
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
|
|
191
218
|
VARIATIONS["FirefoxTransitional"]
|
|
192
219
|
else
|
|
193
220
|
VARIATIONS[@parsed_ua.browser]
|
|
@@ -198,7 +225,7 @@ module SecureHeaders
|
|
|
198
225
|
end
|
|
199
226
|
|
|
200
227
|
def nonces_supported?
|
|
201
|
-
@nonces_supported ||=
|
|
228
|
+
@nonces_supported ||= self.class.nonces_supported?(@parsed_ua)
|
|
202
229
|
end
|
|
203
230
|
|
|
204
231
|
def symbol_to_hyphen_case(sym)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
module SecureHeaders
|
|
2
|
+
module DynamicConfig
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.send(:attr_writer, :modified)
|
|
5
|
+
base.send(:attr_reader, *base.attrs)
|
|
6
|
+
base.attrs.each do |attr|
|
|
7
|
+
base.send(:define_method, "#{attr}=") do |value|
|
|
8
|
+
if self.class.attrs.include?(attr)
|
|
9
|
+
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
|
10
|
+
prev_value = self.instance_variable_get("@#{attr}")
|
|
11
|
+
self.instance_variable_set("@#{attr}", value)
|
|
12
|
+
if prev_value != self.instance_variable_get("@#{attr}")
|
|
13
|
+
@modified = true
|
|
14
|
+
end
|
|
15
|
+
else
|
|
16
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(hash)
|
|
23
|
+
from_hash(hash)
|
|
24
|
+
@modified = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def update_directive(directive, value)
|
|
28
|
+
self.send("#{directive}=", value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def directive_value(directive)
|
|
32
|
+
if self.class.attrs.include?(directive)
|
|
33
|
+
self.send(directive)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def modified?
|
|
38
|
+
@modified
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def merge(new_hash)
|
|
42
|
+
ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def merge!(new_hash)
|
|
46
|
+
from_hash(new_hash)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def append(new_hash)
|
|
50
|
+
from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
self.class.attrs.each_with_object({}) do |key, hash|
|
|
55
|
+
hash[key] = self.send(key)
|
|
56
|
+
end.reject { |_, v| v.nil? }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dup
|
|
60
|
+
self.class.new(self.to_h)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def opt_out?
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def ==(o)
|
|
68
|
+
self.class == o.class && self.to_h == o.to_h
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
alias_method :[], :directive_value
|
|
72
|
+
alias_method :[]=, :update_directive
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
def from_hash(hash)
|
|
76
|
+
hash.keys.reject { |k| hash[k].nil? }.map do |k|
|
|
77
|
+
if self.class.attrs.include?(k)
|
|
78
|
+
self.send("#{k}=", hash[k])
|
|
79
|
+
else
|
|
80
|
+
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{hash[k]}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
|
87
|
+
class ContentSecurityPolicyConfig
|
|
88
|
+
CONFIG_KEY = :csp
|
|
89
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
|
90
|
+
|
|
91
|
+
def self.attrs
|
|
92
|
+
PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
include DynamicConfig
|
|
96
|
+
|
|
97
|
+
# based on what was suggested in https://github.com/rails/rails/pull/24961/files
|
|
98
|
+
DEFAULT = {
|
|
99
|
+
default_src: %w('self' https:),
|
|
100
|
+
font_src: %w('self' https: data:),
|
|
101
|
+
img_src: %w('self' https: data:),
|
|
102
|
+
object_src: %w('none'),
|
|
103
|
+
script_src: %w(https:),
|
|
104
|
+
style_src: %w('self' https: 'unsafe-inline')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def report_only?
|
|
108
|
+
false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def make_report_only
|
|
112
|
+
ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
|
|
117
|
+
CONFIG_KEY = :csp_report_only
|
|
118
|
+
HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
|
|
119
|
+
|
|
120
|
+
def report_only?
|
|
121
|
+
true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def make_report_only
|
|
125
|
+
self
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -7,9 +7,6 @@ module SecureHeaders
|
|
|
7
7
|
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
8
8
|
DEFAULT_VALUE = "default-src https:".freeze
|
|
9
9
|
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
10
|
-
HEADER_NAME = "Content-Security-Policy".freeze
|
|
11
|
-
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
12
|
-
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
13
10
|
DATA_PROTOCOL = "data:".freeze
|
|
14
11
|
BLOB_PROTOCOL = "blob:".freeze
|
|
15
12
|
SELF = "'self'".freeze
|
|
@@ -17,6 +14,7 @@ module SecureHeaders
|
|
|
17
14
|
STAR = "*".freeze
|
|
18
15
|
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
19
16
|
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
17
|
+
STRICT_DYNAMIC = "'strict-dynamic'".freeze
|
|
20
18
|
|
|
21
19
|
# leftover deprecated values that will be in common use upon upgrading.
|
|
22
20
|
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
|
@@ -53,16 +51,6 @@ module SecureHeaders
|
|
|
53
51
|
FRAME_ANCESTORS = :frame_ancestors
|
|
54
52
|
PLUGIN_TYPES = :plugin_types
|
|
55
53
|
|
|
56
|
-
# These are directives that do not inherit the default-src value. This is
|
|
57
|
-
# useful when calling #combine_policies.
|
|
58
|
-
NON_FETCH_SOURCES = [
|
|
59
|
-
BASE_URI,
|
|
60
|
-
FORM_ACTION,
|
|
61
|
-
FRAME_ANCESTORS,
|
|
62
|
-
PLUGIN_TYPES,
|
|
63
|
-
REPORT_URI
|
|
64
|
-
]
|
|
65
|
-
|
|
66
54
|
DIRECTIVES_2_0 = [
|
|
67
55
|
DIRECTIVES_1_0,
|
|
68
56
|
BASE_URI,
|
|
@@ -127,6 +115,18 @@ module SecureHeaders
|
|
|
127
115
|
# everything else is in between.
|
|
128
116
|
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
129
117
|
|
|
118
|
+
# These are directives that do not inherit the default-src value. This is
|
|
119
|
+
# useful when calling #combine_policies.
|
|
120
|
+
NON_FETCH_SOURCES = [
|
|
121
|
+
BASE_URI,
|
|
122
|
+
FORM_ACTION,
|
|
123
|
+
FRAME_ANCESTORS,
|
|
124
|
+
PLUGIN_TYPES,
|
|
125
|
+
REPORT_URI
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES
|
|
129
|
+
|
|
130
130
|
VARIATIONS = {
|
|
131
131
|
"Chrome" => CHROME_DIRECTIVES,
|
|
132
132
|
"Opera" => CHROME_DIRECTIVES,
|
|
@@ -156,13 +156,13 @@ module SecureHeaders
|
|
|
156
156
|
PLUGIN_TYPES => :source_list,
|
|
157
157
|
REFLECTED_XSS => :string,
|
|
158
158
|
REPORT_URI => :source_list,
|
|
159
|
-
SANDBOX => :
|
|
159
|
+
SANDBOX => :source_list,
|
|
160
160
|
SCRIPT_SRC => :source_list,
|
|
161
161
|
STYLE_SRC => :source_list,
|
|
162
162
|
UPGRADE_INSECURE_REQUESTS => :boolean
|
|
163
163
|
}.freeze
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
|
|
166
166
|
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
167
167
|
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
168
168
|
|
|
@@ -179,6 +179,11 @@ module SecureHeaders
|
|
|
179
179
|
:preserve_schemes
|
|
180
180
|
].freeze
|
|
181
181
|
|
|
182
|
+
NONCES = [
|
|
183
|
+
:script_nonce,
|
|
184
|
+
:style_nonce
|
|
185
|
+
].freeze
|
|
186
|
+
|
|
182
187
|
module ClassMethods
|
|
183
188
|
# Public: generate a header name, value array that is user-agent-aware.
|
|
184
189
|
#
|
|
@@ -194,9 +199,11 @@ module SecureHeaders
|
|
|
194
199
|
# Does not validate the invididual values of the source expression (e.g.
|
|
195
200
|
# script_src => h*t*t*p: will not raise an exception)
|
|
196
201
|
def validate_config!(config)
|
|
197
|
-
return if config.nil? || config
|
|
198
|
-
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config
|
|
199
|
-
|
|
202
|
+
return if config.nil? || config.opt_out?
|
|
203
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src)
|
|
204
|
+
ContentSecurityPolicyConfig.attrs.each do |key|
|
|
205
|
+
value = config.directive_value(key)
|
|
206
|
+
next unless value
|
|
200
207
|
if META_CONFIGS.include?(key)
|
|
201
208
|
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
202
209
|
else
|
|
@@ -205,16 +212,13 @@ module SecureHeaders
|
|
|
205
212
|
end
|
|
206
213
|
end
|
|
207
214
|
|
|
208
|
-
# Public:
|
|
209
|
-
# actual value of the config.
|
|
215
|
+
# Public: check if a user agent supports CSP nonces
|
|
210
216
|
#
|
|
211
|
-
#
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return false if config == OPT_OUT
|
|
217
|
-
config == combine_policies(config, additions)
|
|
217
|
+
# user_agent - a String or a UserAgent object
|
|
218
|
+
def nonces_supported?(user_agent)
|
|
219
|
+
user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String)
|
|
220
|
+
MODERN_BROWSERS.include?(user_agent.browser) ||
|
|
221
|
+
user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10
|
|
218
222
|
end
|
|
219
223
|
|
|
220
224
|
# Public: combine the values from two different configs.
|
|
@@ -231,7 +235,7 @@ module SecureHeaders
|
|
|
231
235
|
# 3. if a value in additions does exist in the original config, the two
|
|
232
236
|
# values are joined.
|
|
233
237
|
def combine_policies(original, additions)
|
|
234
|
-
if original ==
|
|
238
|
+
if original == {}
|
|
235
239
|
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
236
240
|
end
|
|
237
241
|
|
|
@@ -268,8 +272,23 @@ module SecureHeaders
|
|
|
268
272
|
def populate_fetch_source_with_default!(original, additions)
|
|
269
273
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
270
274
|
additions.keys.each do |directive|
|
|
271
|
-
|
|
272
|
-
original
|
|
275
|
+
if !original[directive] && ((source_list?(directive) && FETCH_SOURCES.include?(directive)) || nonce_added?(original, additions))
|
|
276
|
+
if nonce_added?(original, additions)
|
|
277
|
+
inferred_directive = directive.to_s.gsub(/_nonce/, "_src").to_sym
|
|
278
|
+
unless original[inferred_directive] || NON_FETCH_SOURCES.include?(inferred_directive)
|
|
279
|
+
original[inferred_directive] = original[:default_src]
|
|
280
|
+
end
|
|
281
|
+
else
|
|
282
|
+
original[directive] = original[:default_src]
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def nonce_added?(original, additions)
|
|
289
|
+
[:script_nonce, :style_nonce].each do |nonce|
|
|
290
|
+
if additions[nonce] && !original[nonce]
|
|
291
|
+
return true
|
|
273
292
|
end
|
|
274
293
|
end
|
|
275
294
|
end
|
|
@@ -34,6 +34,14 @@ module SecureHeaders
|
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def content_security_policy_script_nonce
|
|
38
|
+
content_security_policy_nonce(:script)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def content_security_policy_style_nonce
|
|
42
|
+
content_security_policy_nonce(:style)
|
|
43
|
+
end
|
|
44
|
+
|
|
37
45
|
##
|
|
38
46
|
# Checks to see if the hashed code is expected and adds the hash source
|
|
39
47
|
# value to the current CSP.
|