secure_headers 6.0.0 → 6.7.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 +5 -5
- data/.github/dependabot.yml +6 -0
- data/.github/workflows/build.yml +24 -0
- data/.github/workflows/github-release.yml +28 -0
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +47 -1
- data/Gemfile +4 -1
- data/LICENSE +1 -1
- data/README.md +70 -30
- data/docs/cookies.md +5 -4
- data/docs/named_overrides_and_appends.md +3 -6
- data/docs/per_action_configuration.md +2 -4
- data/docs/upgrading-to-6-0.md +4 -4
- data/lib/secure_headers/configuration.rb +24 -16
- data/lib/secure_headers/headers/content_security_policy.rb +34 -41
- data/lib/secure_headers/headers/content_security_policy_config.rb +15 -48
- data/lib/secure_headers/headers/cookie.rb +9 -3
- data/lib/secure_headers/headers/policy_management.rb +92 -17
- data/lib/secure_headers/middleware.rb +0 -6
- data/lib/secure_headers/utils/cookies_config.rb +7 -5
- data/lib/secure_headers/version.rb +5 -0
- data/lib/secure_headers/view_helper.rb +11 -10
- data/lib/secure_headers.rb +3 -11
- data/lib/tasks/tasks.rake +6 -7
- data/secure_headers.gemspec +9 -5
- data/spec/lib/secure_headers/configuration_spec.rb +54 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +98 -8
- data/spec/lib/secure_headers/headers/cookie_spec.rb +22 -25
- data/spec/lib/secure_headers/headers/policy_management_spec.rb +29 -19
- data/spec/lib/secure_headers/middleware_spec.rb +0 -19
- data/spec/lib/secure_headers/view_helpers_spec.rb +5 -4
- data/spec/lib/secure_headers_spec.rb +0 -35
- data/spec/spec_helper.rb +10 -1
- metadata +13 -12
- data/.travis.yml +0 -29
- data/lib/secure_headers/headers/public_key_pins.rb +0 -81
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +0 -38
@@ -7,21 +7,22 @@ module SecureHeaders
|
|
7
7
|
include PolicyManagement
|
8
8
|
|
9
9
|
def initialize(config = nil)
|
10
|
-
@config =
|
11
|
-
if config
|
12
|
-
|
10
|
+
@config =
|
11
|
+
if config.is_a?(Hash)
|
12
|
+
if config[:report_only]
|
13
|
+
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
|
14
|
+
else
|
15
|
+
ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG)
|
16
|
+
end
|
17
|
+
elsif config.nil?
|
18
|
+
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
13
19
|
else
|
14
|
-
|
20
|
+
config
|
15
21
|
end
|
16
|
-
elsif config.nil?
|
17
|
-
ContentSecurityPolicyConfig.new(DEFAULT_CONFIG)
|
18
|
-
else
|
19
|
-
config
|
20
|
-
end
|
21
22
|
|
22
|
-
@preserve_schemes = @config
|
23
|
-
@script_nonce = @config
|
24
|
-
@style_nonce = @config
|
23
|
+
@preserve_schemes = @config[:preserve_schemes]
|
24
|
+
@script_nonce = @config[:script_nonce]
|
25
|
+
@style_nonce = @config[:style_nonce]
|
25
26
|
end
|
26
27
|
|
27
28
|
##
|
@@ -34,11 +35,12 @@ module SecureHeaders
|
|
34
35
|
##
|
35
36
|
# Return the value of the CSP header
|
36
37
|
def value
|
37
|
-
@value ||=
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
38
|
+
@value ||=
|
39
|
+
if @config
|
40
|
+
build_value
|
41
|
+
else
|
42
|
+
DEFAULT_VALUE
|
43
|
+
end
|
42
44
|
end
|
43
45
|
|
44
46
|
private
|
@@ -51,14 +53,16 @@ module SecureHeaders
|
|
51
53
|
def build_value
|
52
54
|
directives.map do |directive_name|
|
53
55
|
case DIRECTIVE_VALUE_TYPES[directive_name]
|
56
|
+
when :source_list,
|
57
|
+
:require_sri_for_list, # require_sri is a simple set of strings that don't need to deal with symbol casing
|
58
|
+
:require_trusted_types_for_list
|
59
|
+
build_source_list_directive(directive_name)
|
54
60
|
when :boolean
|
55
61
|
symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name)
|
56
62
|
when :sandbox_list
|
57
63
|
build_sandbox_list_directive(directive_name)
|
58
64
|
when :media_type_list
|
59
65
|
build_media_type_list_directive(directive_name)
|
60
|
-
when :source_list
|
61
|
-
build_source_list_directive(directive_name)
|
62
66
|
end
|
63
67
|
end.compact.join("; ")
|
64
68
|
end
|
@@ -103,10 +107,15 @@ module SecureHeaders
|
|
103
107
|
# Returns a string representing a directive.
|
104
108
|
def build_source_list_directive(directive)
|
105
109
|
source_list = @config.directive_value(directive)
|
106
|
-
|
107
110
|
if source_list != OPT_OUT && source_list && source_list.any?
|
108
|
-
|
109
|
-
|
111
|
+
minified_source_list = minify_source_list(directive, source_list).join(" ")
|
112
|
+
|
113
|
+
if minified_source_list =~ /(\n|;)/
|
114
|
+
Kernel.warn("#{directive} contains a #{$1} in #{minified_source_list.inspect} which will raise an error in future versions. It has been replaced with a blank space.")
|
115
|
+
end
|
116
|
+
|
117
|
+
escaped_source_list = minified_source_list.gsub(/[\n;]/, " ")
|
118
|
+
[symbol_to_hyphen_case(directive), escaped_source_list].join(" ").strip
|
110
119
|
end
|
111
120
|
end
|
112
121
|
|
@@ -124,7 +133,7 @@ module SecureHeaders
|
|
124
133
|
unless directive == REPORT_URI || @preserve_schemes
|
125
134
|
source_list = strip_source_schemes(source_list)
|
126
135
|
end
|
127
|
-
|
136
|
+
source_list.uniq
|
128
137
|
end
|
129
138
|
end
|
130
139
|
|
@@ -142,23 +151,6 @@ module SecureHeaders
|
|
142
151
|
end
|
143
152
|
end
|
144
153
|
|
145
|
-
# Removes duplicates and sources that already match an existing wild card.
|
146
|
-
#
|
147
|
-
# e.g. *.github.com asdf.github.com becomes *.github.com
|
148
|
-
def dedup_source_list(sources)
|
149
|
-
sources = sources.uniq
|
150
|
-
wild_sources = sources.select { |source| source =~ STAR_REGEXP }
|
151
|
-
|
152
|
-
if wild_sources.any?
|
153
|
-
sources.reject do |source|
|
154
|
-
!wild_sources.include?(source) &&
|
155
|
-
wild_sources.any? { |pattern| File.fnmatch(pattern, source) }
|
156
|
-
end
|
157
|
-
else
|
158
|
-
sources
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
154
|
# Private: append a nonce to the script/style directories if script_nonce
|
163
155
|
# or style_nonce are provided.
|
164
156
|
def populate_nonces(directive, source_list)
|
@@ -179,7 +171,8 @@ module SecureHeaders
|
|
179
171
|
# unsafe-inline, this is more concise.
|
180
172
|
def append_nonce(source_list, nonce)
|
181
173
|
if nonce
|
182
|
-
source_list.push("'nonce-#{nonce}'"
|
174
|
+
source_list.push("'nonce-#{nonce}'")
|
175
|
+
source_list.push(UNSAFE_INLINE) unless @config[:disable_nonce_backwards_compatibility]
|
183
176
|
end
|
184
177
|
|
185
178
|
source_list
|
@@ -1,56 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module SecureHeaders
|
3
3
|
module DynamicConfig
|
4
|
-
def self.included(base)
|
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
|
-
write_attribute(attr, value)
|
10
|
-
else
|
11
|
-
raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
4
|
def initialize(hash)
|
18
|
-
@
|
19
|
-
@block_all_mixed_content = nil
|
20
|
-
@child_src = nil
|
21
|
-
@connect_src = nil
|
22
|
-
@default_src = nil
|
23
|
-
@font_src = nil
|
24
|
-
@form_action = nil
|
25
|
-
@frame_ancestors = nil
|
26
|
-
@frame_src = nil
|
27
|
-
@img_src = nil
|
28
|
-
@manifest_src = nil
|
29
|
-
@media_src = nil
|
30
|
-
@object_src = nil
|
31
|
-
@plugin_types = nil
|
32
|
-
@preserve_schemes = nil
|
33
|
-
@report_only = nil
|
34
|
-
@report_uri = nil
|
35
|
-
@sandbox = nil
|
36
|
-
@script_nonce = nil
|
37
|
-
@script_src = nil
|
38
|
-
@style_nonce = nil
|
39
|
-
@style_src = nil
|
40
|
-
@worker_src = nil
|
41
|
-
@upgrade_insecure_requests = nil
|
5
|
+
@config = {}
|
42
6
|
|
43
7
|
from_hash(hash)
|
44
8
|
end
|
45
9
|
|
10
|
+
def initialize_copy(hash)
|
11
|
+
@config = hash.to_h
|
12
|
+
end
|
13
|
+
|
46
14
|
def update_directive(directive, value)
|
47
|
-
|
15
|
+
@config[directive] = value
|
48
16
|
end
|
49
17
|
|
50
18
|
def directive_value(directive)
|
51
|
-
|
52
|
-
|
53
|
-
end
|
19
|
+
# No need to check attrs, as we only assign valid keys
|
20
|
+
@config[directive]
|
54
21
|
end
|
55
22
|
|
56
23
|
def merge(new_hash)
|
@@ -68,10 +35,7 @@ module SecureHeaders
|
|
68
35
|
end
|
69
36
|
|
70
37
|
def to_h
|
71
|
-
|
72
|
-
value = self.send(key)
|
73
|
-
hash[key] = value unless value.nil?
|
74
|
-
end
|
38
|
+
@config.dup
|
75
39
|
end
|
76
40
|
|
77
41
|
def dup
|
@@ -104,8 +68,11 @@ module SecureHeaders
|
|
104
68
|
|
105
69
|
def write_attribute(attr, value)
|
106
70
|
value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
|
107
|
-
|
108
|
-
|
71
|
+
if value.nil?
|
72
|
+
@config.delete(attr)
|
73
|
+
else
|
74
|
+
@config[attr] = value
|
75
|
+
end
|
109
76
|
end
|
110
77
|
end
|
111
78
|
|
@@ -113,7 +80,7 @@ module SecureHeaders
|
|
113
80
|
class ContentSecurityPolicyConfig
|
114
81
|
HEADER_NAME = "Content-Security-Policy".freeze
|
115
82
|
|
116
|
-
ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
|
83
|
+
ATTRS = Set.new(PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES)
|
117
84
|
def self.attrs
|
118
85
|
ATTRS
|
119
86
|
end
|
@@ -80,9 +80,9 @@ module SecureHeaders
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def conditionally_flag?(configuration)
|
83
|
-
if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
|
83
|
+
if (Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
|
84
84
|
true
|
85
|
-
elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
|
85
|
+
elsif (Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
|
86
86
|
true
|
87
87
|
else
|
88
88
|
false
|
@@ -94,12 +94,14 @@ module SecureHeaders
|
|
94
94
|
"SameSite=Lax"
|
95
95
|
elsif flag_samesite_strict?
|
96
96
|
"SameSite=Strict"
|
97
|
+
elsif flag_samesite_none?
|
98
|
+
"SameSite=None"
|
97
99
|
end
|
98
100
|
end
|
99
101
|
|
100
102
|
def flag_samesite?
|
101
103
|
return false if config == OPT_OUT || config[:samesite] == OPT_OUT
|
102
|
-
flag_samesite_lax? || flag_samesite_strict?
|
104
|
+
flag_samesite_lax? || flag_samesite_strict? || flag_samesite_none?
|
103
105
|
end
|
104
106
|
|
105
107
|
def flag_samesite_lax?
|
@@ -110,6 +112,10 @@ module SecureHeaders
|
|
110
112
|
flag_samesite_enforcement?(:strict)
|
111
113
|
end
|
112
114
|
|
115
|
+
def flag_samesite_none?
|
116
|
+
flag_samesite_enforcement?(:none)
|
117
|
+
end
|
118
|
+
|
113
119
|
def flag_samesite_enforcement?(mode)
|
114
120
|
return unless config[:samesite]
|
115
121
|
|
@@ -1,4 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
2
5
|
module SecureHeaders
|
3
6
|
module PolicyManagement
|
4
7
|
def self.included(base)
|
@@ -68,20 +71,44 @@ module SecureHeaders
|
|
68
71
|
|
69
72
|
# All the directives currently under consideration for CSP level 3.
|
70
73
|
# https://w3c.github.io/webappsec/specs/CSP2/
|
71
|
-
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
72
74
|
MANIFEST_SRC = :manifest_src
|
75
|
+
NAVIGATE_TO = :navigate_to
|
76
|
+
PREFETCH_SRC = :prefetch_src
|
77
|
+
REQUIRE_SRI_FOR = :require_sri_for
|
73
78
|
UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests
|
74
79
|
WORKER_SRC = :worker_src
|
80
|
+
SCRIPT_SRC_ELEM = :script_src_elem
|
81
|
+
SCRIPT_SRC_ATTR = :script_src_attr
|
82
|
+
STYLE_SRC_ELEM = :style_src_elem
|
83
|
+
STYLE_SRC_ATTR = :style_src_attr
|
75
84
|
|
76
85
|
DIRECTIVES_3_0 = [
|
77
86
|
DIRECTIVES_2_0,
|
78
|
-
BLOCK_ALL_MIXED_CONTENT,
|
79
87
|
MANIFEST_SRC,
|
88
|
+
NAVIGATE_TO,
|
89
|
+
PREFETCH_SRC,
|
90
|
+
REQUIRE_SRI_FOR,
|
80
91
|
WORKER_SRC,
|
81
|
-
UPGRADE_INSECURE_REQUESTS
|
92
|
+
UPGRADE_INSECURE_REQUESTS,
|
93
|
+
SCRIPT_SRC_ELEM,
|
94
|
+
SCRIPT_SRC_ATTR,
|
95
|
+
STYLE_SRC_ELEM,
|
96
|
+
STYLE_SRC_ATTR
|
82
97
|
].flatten.freeze
|
83
98
|
|
84
|
-
|
99
|
+
# Experimental directives - these vary greatly in support
|
100
|
+
# See MDN for details.
|
101
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/trusted-types
|
102
|
+
TRUSTED_TYPES = :trusted_types
|
103
|
+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/require-trusted-types-for
|
104
|
+
REQUIRE_TRUSTED_TYPES_FOR = :require_trusted_types_for
|
105
|
+
|
106
|
+
DIRECTIVES_EXPERIMENTAL = [
|
107
|
+
TRUSTED_TYPES,
|
108
|
+
REQUIRE_TRUSTED_TYPES_FOR,
|
109
|
+
].flatten.freeze
|
110
|
+
|
111
|
+
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_EXPERIMENTAL).uniq.sort
|
85
112
|
|
86
113
|
# Think of default-src and report-uri as the beginning and end respectively,
|
87
114
|
# everything else is in between.
|
@@ -89,7 +116,6 @@ module SecureHeaders
|
|
89
116
|
|
90
117
|
DIRECTIVE_VALUE_TYPES = {
|
91
118
|
BASE_URI => :source_list,
|
92
|
-
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
93
119
|
CHILD_SRC => :source_list,
|
94
120
|
CONNECT_SRC => :source_list,
|
95
121
|
DEFAULT_SRC => :source_list,
|
@@ -100,14 +126,23 @@ module SecureHeaders
|
|
100
126
|
IMG_SRC => :source_list,
|
101
127
|
MANIFEST_SRC => :source_list,
|
102
128
|
MEDIA_SRC => :source_list,
|
129
|
+
NAVIGATE_TO => :source_list,
|
103
130
|
OBJECT_SRC => :source_list,
|
104
131
|
PLUGIN_TYPES => :media_type_list,
|
132
|
+
REQUIRE_SRI_FOR => :require_sri_for_list,
|
133
|
+
REQUIRE_TRUSTED_TYPES_FOR => :require_trusted_types_for_list,
|
105
134
|
REPORT_URI => :source_list,
|
135
|
+
PREFETCH_SRC => :source_list,
|
106
136
|
SANDBOX => :sandbox_list,
|
107
137
|
SCRIPT_SRC => :source_list,
|
138
|
+
SCRIPT_SRC_ELEM => :source_list,
|
139
|
+
SCRIPT_SRC_ATTR => :source_list,
|
108
140
|
STYLE_SRC => :source_list,
|
141
|
+
STYLE_SRC_ELEM => :source_list,
|
142
|
+
STYLE_SRC_ATTR => :source_list,
|
143
|
+
TRUSTED_TYPES => :source_list,
|
109
144
|
WORKER_SRC => :source_list,
|
110
|
-
UPGRADE_INSECURE_REQUESTS => :boolean
|
145
|
+
UPGRADE_INSECURE_REQUESTS => :boolean,
|
111
146
|
}.freeze
|
112
147
|
|
113
148
|
# These are directives that don't have use a source list, and hence do not
|
@@ -122,7 +157,8 @@ module SecureHeaders
|
|
122
157
|
BASE_URI,
|
123
158
|
FORM_ACTION,
|
124
159
|
FRAME_ANCESTORS,
|
125
|
-
|
160
|
+
NAVIGATE_TO,
|
161
|
+
REPORT_URI,
|
126
162
|
]
|
127
163
|
|
128
164
|
FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES
|
@@ -140,7 +176,8 @@ module SecureHeaders
|
|
140
176
|
|
141
177
|
META_CONFIGS = [
|
142
178
|
:report_only,
|
143
|
-
:preserve_schemes
|
179
|
+
:preserve_schemes,
|
180
|
+
:disable_nonce_backwards_compatibility
|
144
181
|
].freeze
|
145
182
|
|
146
183
|
NONCES = [
|
@@ -148,6 +185,9 @@ module SecureHeaders
|
|
148
185
|
:style_nonce
|
149
186
|
].freeze
|
150
187
|
|
188
|
+
REQUIRE_SRI_FOR_VALUES = Set.new(%w(script style))
|
189
|
+
REQUIRE_TRUSTED_TYPES_FOR_VALUES = Set.new(%w('script'))
|
190
|
+
|
151
191
|
module ClassMethods
|
152
192
|
# Public: generate a header name, value array that is user-agent-aware.
|
153
193
|
#
|
@@ -198,7 +238,7 @@ module SecureHeaders
|
|
198
238
|
#
|
199
239
|
# raises an error if the original config is OPT_OUT
|
200
240
|
#
|
201
|
-
# 1. for non-source-list values (report_only,
|
241
|
+
# 1. for non-source-list values (report_only, upgrade_insecure_requests),
|
202
242
|
# additions will overwrite the original value.
|
203
243
|
# 2. if a value in additions does not exist in the original config, the
|
204
244
|
# default-src value is included to match original behavior.
|
@@ -241,7 +281,9 @@ module SecureHeaders
|
|
241
281
|
def list_directive?(directive)
|
242
282
|
source_list?(directive) ||
|
243
283
|
sandbox_list?(directive) ||
|
244
|
-
media_type_list?(directive)
|
284
|
+
media_type_list?(directive) ||
|
285
|
+
require_sri_for_list?(directive) ||
|
286
|
+
require_trusted_types_for_list?(directive)
|
245
287
|
end
|
246
288
|
|
247
289
|
# For each directive in additions that does not exist in the original config,
|
@@ -249,11 +291,12 @@ module SecureHeaders
|
|
249
291
|
def populate_fetch_source_with_default!(original, additions)
|
250
292
|
# in case we would be appending to an empty directive, fill it with the default-src value
|
251
293
|
additions.each_key do |directive|
|
252
|
-
directive =
|
253
|
-
directive.to_s.
|
254
|
-
|
255
|
-
|
256
|
-
|
294
|
+
directive =
|
295
|
+
if directive.to_s.end_with?("_nonce")
|
296
|
+
directive.to_s.gsub(/_nonce/, "_src").to_sym
|
297
|
+
else
|
298
|
+
directive
|
299
|
+
end
|
257
300
|
# Don't set a default if directive has an existing value
|
258
301
|
next if original[directive]
|
259
302
|
if FETCH_SOURCES.include?(directive)
|
@@ -274,11 +317,21 @@ module SecureHeaders
|
|
274
317
|
DIRECTIVE_VALUE_TYPES[directive] == :media_type_list
|
275
318
|
end
|
276
319
|
|
320
|
+
def require_sri_for_list?(directive)
|
321
|
+
DIRECTIVE_VALUE_TYPES[directive] == :require_sri_for_list
|
322
|
+
end
|
323
|
+
|
324
|
+
def require_trusted_types_for_list?(directive)
|
325
|
+
DIRECTIVE_VALUE_TYPES[directive] == :require_trusted_types_for_list
|
326
|
+
end
|
327
|
+
|
277
328
|
# Private: Validates that the configuration has a valid type, or that it is a valid
|
278
329
|
# source expression.
|
279
330
|
def validate_directive!(directive, value)
|
280
331
|
ensure_valid_directive!(directive)
|
281
332
|
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive]
|
333
|
+
when :source_list
|
334
|
+
validate_source_expression!(directive, value)
|
282
335
|
when :boolean
|
283
336
|
unless boolean?(value)
|
284
337
|
raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value")
|
@@ -287,8 +340,10 @@ module SecureHeaders
|
|
287
340
|
validate_sandbox_expression!(directive, value)
|
288
341
|
when :media_type_list
|
289
342
|
validate_media_type_expression!(directive, value)
|
290
|
-
when :
|
291
|
-
|
343
|
+
when :require_sri_for_list
|
344
|
+
validate_require_sri_source_expression!(directive, value)
|
345
|
+
when :require_trusted_types_for_list
|
346
|
+
validate_require_trusted_types_for_source_expression!(directive, value)
|
292
347
|
else
|
293
348
|
raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}")
|
294
349
|
end
|
@@ -323,6 +378,26 @@ module SecureHeaders
|
|
323
378
|
end
|
324
379
|
end
|
325
380
|
|
381
|
+
# Private: validates that a require sri for expression:
|
382
|
+
# 1. is an array of strings
|
383
|
+
# 2. is a subset of ["string", "style"]
|
384
|
+
def validate_require_sri_source_expression!(directive, require_sri_for_expression)
|
385
|
+
ensure_array_of_strings!(directive, require_sri_for_expression)
|
386
|
+
unless require_sri_for_expression.to_set.subset?(REQUIRE_SRI_FOR_VALUES)
|
387
|
+
raise ContentSecurityPolicyConfigError.new(%(require-sri for must be a subset of #{REQUIRE_SRI_FOR_VALUES.to_a} but was #{require_sri_for_expression}))
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# Private: validates that a require trusted types for expression:
|
392
|
+
# 1. is an array of strings
|
393
|
+
# 2. is a subset of ["'script'"]
|
394
|
+
def validate_require_trusted_types_for_source_expression!(directive, require_trusted_types_for_expression)
|
395
|
+
ensure_array_of_strings!(directive, require_trusted_types_for_expression)
|
396
|
+
unless require_trusted_types_for_expression.to_set.subset?(REQUIRE_TRUSTED_TYPES_FOR_VALUES)
|
397
|
+
raise ContentSecurityPolicyConfigError.new(%(require-trusted-types-for for must be a subset of #{REQUIRE_TRUSTED_TYPES_FOR_VALUES.to_a} but was #{require_trusted_types_for_expression}))
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
326
401
|
# Private: validates that a source expression:
|
327
402
|
# 1. is an array of strings
|
328
403
|
# 2. does not contain any deprecated, now invalid values (inline, eval, self, none)
|
@@ -1,8 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
module SecureHeaders
|
3
3
|
class Middleware
|
4
|
-
HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166"
|
5
|
-
|
6
4
|
def initialize(app)
|
7
5
|
@app = app
|
8
6
|
end
|
@@ -13,10 +11,6 @@ module SecureHeaders
|
|
13
11
|
status, headers, response = @app.call(env)
|
14
12
|
|
15
13
|
config = SecureHeaders.config_for(req)
|
16
|
-
if config.hpkp_report_host == req.host
|
17
|
-
Kernel.warn(HPKP_SAME_HOST_WARNING)
|
18
|
-
end
|
19
|
-
|
20
14
|
flag_cookies!(headers, override_secure(env, config.cookies)) unless config.cookies == OPT_OUT
|
21
15
|
headers.merge!(SecureHeaders.header_hash_for(req))
|
22
16
|
[status, headers, response]
|
@@ -43,10 +43,12 @@ module SecureHeaders
|
|
43
43
|
|
44
44
|
# when configuring with booleans, only one enforcement is permitted
|
45
45
|
def validate_samesite_boolean_config!
|
46
|
-
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict)
|
47
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
48
|
-
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax)
|
49
|
-
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax
|
46
|
+
if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && (config[:samesite].key?(:strict) || config[:samesite].key?(:none))
|
47
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax with strict or no enforcement is not permitted.")
|
48
|
+
elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:none))
|
49
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure strict with lax or no enforcement is not permitted.")
|
50
|
+
elsif config[:samesite].key?(:none) && config[:samesite][:none].is_a?(TrueClass) && (config[:samesite].key?(:lax) || config[:samesite].key?(:strict))
|
51
|
+
raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure no enforcement with lax or strict is not permitted.")
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
@@ -65,7 +67,7 @@ module SecureHeaders
|
|
65
67
|
|
66
68
|
def validate_hash_or_true_or_opt_out!(attribute)
|
67
69
|
if !(is_hash?(config[attribute]) || is_true_or_opt_out?(config[attribute]))
|
68
|
-
raise CookiesConfigError.new("#{attribute} cookie config must be a hash or
|
70
|
+
raise CookiesConfigError.new("#{attribute} cookie config must be a hash, true, or SecureHeaders::OPT_OUT")
|
69
71
|
end
|
70
72
|
end
|
71
73
|
|
@@ -21,7 +21,7 @@ module SecureHeaders
|
|
21
21
|
def nonced_stylesheet_link_tag(*args, &block)
|
22
22
|
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
23
23
|
|
24
|
-
stylesheet_link_tag(*args, opts, &block)
|
24
|
+
stylesheet_link_tag(*args, **opts, &block)
|
25
25
|
end
|
26
26
|
|
27
27
|
# Public: create a script tag using the content security policy nonce.
|
@@ -39,7 +39,7 @@ module SecureHeaders
|
|
39
39
|
def nonced_javascript_include_tag(*args, &block)
|
40
40
|
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
41
41
|
|
42
|
-
javascript_include_tag(*args, opts, &block)
|
42
|
+
javascript_include_tag(*args, **opts, &block)
|
43
43
|
end
|
44
44
|
|
45
45
|
# Public: create a script Webpacker pack tag using the content security policy nonce.
|
@@ -49,7 +49,7 @@ module SecureHeaders
|
|
49
49
|
def nonced_javascript_pack_tag(*args, &block)
|
50
50
|
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
|
51
51
|
|
52
|
-
javascript_pack_tag(*args, opts, &block)
|
52
|
+
javascript_pack_tag(*args, **opts, &block)
|
53
53
|
end
|
54
54
|
|
55
55
|
# Public: create a stylesheet Webpacker link tag using the content security policy nonce.
|
@@ -59,7 +59,7 @@ module SecureHeaders
|
|
59
59
|
def nonced_stylesheet_pack_tag(*args, &block)
|
60
60
|
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
|
61
61
|
|
62
|
-
stylesheet_pack_tag(*args, opts, &block)
|
62
|
+
stylesheet_pack_tag(*args, **opts, &block)
|
63
63
|
end
|
64
64
|
|
65
65
|
# Public: use the content security policy nonce for this request directly.
|
@@ -147,12 +147,13 @@ module SecureHeaders
|
|
147
147
|
|
148
148
|
def nonced_tag(type, content_or_options, block)
|
149
149
|
options = {}
|
150
|
-
content =
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
150
|
+
content =
|
151
|
+
if block
|
152
|
+
options = content_or_options
|
153
|
+
capture(&block)
|
154
|
+
else
|
155
|
+
content_or_options.html_safe # :'(
|
156
|
+
end
|
156
157
|
content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
|
157
158
|
end
|
158
159
|
|
data/lib/secure_headers.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "secure_headers/hash_helper"
|
3
3
|
require "secure_headers/headers/cookie"
|
4
|
-
require "secure_headers/headers/public_key_pins"
|
5
4
|
require "secure_headers/headers/content_security_policy"
|
6
5
|
require "secure_headers/headers/x_frame_options"
|
7
6
|
require "secure_headers/headers/strict_transport_security"
|
@@ -18,8 +17,7 @@ require "secure_headers/view_helper"
|
|
18
17
|
require "singleton"
|
19
18
|
require "secure_headers/configuration"
|
20
19
|
|
21
|
-
#
|
22
|
-
# or ":optout_of_protection" as a config value to disable a given header
|
20
|
+
# Provide SecureHeaders::OPT_OUT as a config value to disable a given header
|
23
21
|
module SecureHeaders
|
24
22
|
class NoOpHeaderConfig
|
25
23
|
include Singleton
|
@@ -51,10 +49,6 @@ module SecureHeaders
|
|
51
49
|
HTTPS = "https".freeze
|
52
50
|
CSP = ContentSecurityPolicy
|
53
51
|
|
54
|
-
# Headers set on http requests (excludes STS and HPKP)
|
55
|
-
HTTPS_HEADER_CLASSES =
|
56
|
-
[StrictTransportSecurity, PublicKeyPins].freeze
|
57
|
-
|
58
52
|
class << self
|
59
53
|
# Public: override a given set of directives for the current request. If a
|
60
54
|
# value already exists for a given directive, it will be overridden.
|
@@ -138,7 +132,7 @@ module SecureHeaders
|
|
138
132
|
# Public: Builds the hash of headers that should be applied base on the
|
139
133
|
# request.
|
140
134
|
#
|
141
|
-
# StrictTransportSecurity
|
135
|
+
# StrictTransportSecurity is not applied to http requests.
|
142
136
|
# See #config_for to determine which config is used for a given request.
|
143
137
|
#
|
144
138
|
# Returns a hash of header names => header values. The value
|
@@ -151,9 +145,7 @@ module SecureHeaders
|
|
151
145
|
headers = config.generate_headers
|
152
146
|
|
153
147
|
if request.scheme != HTTPS
|
154
|
-
|
155
|
-
headers.delete(klass::HEADER_NAME)
|
156
|
-
end
|
148
|
+
headers.delete(StrictTransportSecurity::HEADER_NAME)
|
157
149
|
end
|
158
150
|
headers
|
159
151
|
end
|
data/lib/tasks/tasks.rake
CHANGED
@@ -20,10 +20,11 @@ namespace :secure_headers do
|
|
20
20
|
(is_erb?(filename) && inline_script =~ /<%.*%>/)
|
21
21
|
end
|
22
22
|
|
23
|
-
def find_inline_content(filename, regex, hashes)
|
23
|
+
def find_inline_content(filename, regex, hashes, strip_trailing_whitespace)
|
24
24
|
file = File.read(filename)
|
25
25
|
file.scan(regex) do # TODO don't use gsub
|
26
26
|
inline_script = Regexp.last_match.captures.last
|
27
|
+
inline_script.gsub!(/(\r?\n)[\t ]+\z/, '\1') if strip_trailing_whitespace
|
27
28
|
if dynamic_content?(filename, inline_script)
|
28
29
|
puts "Looks like there's some dynamic content inside of a tag :-/"
|
29
30
|
puts "That pretty much means the hash value will never match."
|
@@ -38,9 +39,8 @@ namespace :secure_headers do
|
|
38
39
|
def generate_inline_script_hashes(filename)
|
39
40
|
hashes = []
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
end
|
42
|
+
find_inline_content(filename, INLINE_SCRIPT_REGEX, hashes, false)
|
43
|
+
find_inline_content(filename, INLINE_HASH_SCRIPT_HELPER_REGEX, hashes, true)
|
44
44
|
|
45
45
|
hashes
|
46
46
|
end
|
@@ -48,9 +48,8 @@ namespace :secure_headers do
|
|
48
48
|
def generate_inline_style_hashes(filename)
|
49
49
|
hashes = []
|
50
50
|
|
51
|
-
|
52
|
-
|
53
|
-
end
|
51
|
+
find_inline_content(filename, INLINE_STYLE_REGEX, hashes, false)
|
52
|
+
find_inline_content(filename, INLINE_HASH_STYLE_HELPER_REGEX, hashes, true)
|
54
53
|
|
55
54
|
hashes
|
56
55
|
end
|