secure_headers 2.5.3 → 3.0.0.pre
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/.rspec +1 -0
- data/.travis.yml +2 -1
- data/Gemfile +9 -16
- data/README.md +154 -331
- data/Rakefile +2 -36
- data/lib/secure_headers/configuration.rb +189 -0
- data/lib/secure_headers/headers/content_security_policy.rb +341 -254
- data/lib/secure_headers/headers/public_key_pins.rb +43 -58
- data/lib/secure_headers/headers/strict_transport_security.rb +21 -49
- data/lib/secure_headers/headers/x_content_type_options.rb +18 -33
- data/lib/secure_headers/headers/x_download_options.rb +18 -33
- data/lib/secure_headers/headers/x_frame_options.rb +24 -34
- data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +19 -34
- data/lib/secure_headers/headers/x_xss_protection.rb +17 -48
- data/lib/secure_headers/middleware.rb +15 -0
- data/lib/secure_headers/padrino.rb +1 -2
- data/lib/secure_headers/railtie.rb +9 -6
- data/lib/secure_headers/view_helper.rb +27 -43
- data/lib/secure_headers.rb +254 -61
- data/secure_headers.gemspec +7 -12
- data/spec/lib/secure_headers/configuration_spec.rb +80 -0
- data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +111 -276
- data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +17 -17
- data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +11 -43
- data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +11 -18
- data/spec/lib/secure_headers/headers/x_download_options_spec.rb +13 -17
- data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +15 -17
- data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +22 -39
- data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +20 -30
- data/spec/lib/secure_headers/middleware_spec.rb +40 -0
- data/spec/lib/secure_headers_spec.rb +201 -339
- data/spec/spec_helper.rb +30 -30
- data/upgrading-to-3-0.md +35 -0
- metadata +14 -100
- data/fixtures/rails_3_2_22/.rspec +0 -1
- data/fixtures/rails_3_2_22/Gemfile +0 -6
- data/fixtures/rails_3_2_22/README.rdoc +0 -261
- data/fixtures/rails_3_2_22/Rakefile +0 -7
- data/fixtures/rails_3_2_22/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_3_2_22/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_3_2_22/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22/config/application.rb +0 -14
- data/fixtures/rails_3_2_22/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22/config/initializers/secure_headers.rb +0 -16
- data/fixtures/rails_3_2_22/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22/config/script_hashes.yml +0 -5
- data/fixtures/rails_3_2_22/config.ru +0 -7
- data/fixtures/rails_3_2_22/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_3_2_22/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22/spec/spec_helper.rb +0 -15
- data/fixtures/rails_3_2_22/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/.rspec +0 -1
- data/fixtures/rails_3_2_22_no_init/Gemfile +0 -6
- data/fixtures/rails_3_2_22_no_init/README.rdoc +0 -261
- data/fixtures/rails_3_2_22_no_init/Rakefile +0 -7
- data/fixtures/rails_3_2_22_no_init/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/app/controllers/other_things_controller.rb +0 -20
- data/fixtures/rails_3_2_22_no_init/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/app/models/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/app/views/layouts/application.html.erb +0 -12
- data/fixtures/rails_3_2_22_no_init/app/views/other_things/index.html.erb +0 -1
- data/fixtures/rails_3_2_22_no_init/app/views/things/index.html.erb +0 -0
- data/fixtures/rails_3_2_22_no_init/config/application.rb +0 -17
- data/fixtures/rails_3_2_22_no_init/config/boot.rb +0 -6
- data/fixtures/rails_3_2_22_no_init/config/environment.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/config/environments/test.rb +0 -37
- data/fixtures/rails_3_2_22_no_init/config/routes.rb +0 -4
- data/fixtures/rails_3_2_22_no_init/config.ru +0 -4
- data/fixtures/rails_3_2_22_no_init/lib/assets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/lib/tasks/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/log/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/spec/controllers/other_things_controller_spec.rb +0 -56
- data/fixtures/rails_3_2_22_no_init/spec/controllers/things_controller_spec.rb +0 -54
- data/fixtures/rails_3_2_22_no_init/spec/spec_helper.rb +0 -5
- data/fixtures/rails_3_2_22_no_init/vendor/assets/javascripts/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/vendor/assets/stylesheets/.gitkeep +0 -0
- data/fixtures/rails_3_2_22_no_init/vendor/plugins/.gitkeep +0 -0
- data/fixtures/rails_4_1_8/Gemfile +0 -5
- data/fixtures/rails_4_1_8/README.rdoc +0 -28
- data/fixtures/rails_4_1_8/Rakefile +0 -6
- data/fixtures/rails_4_1_8/app/controllers/application_controller.rb +0 -4
- data/fixtures/rails_4_1_8/app/controllers/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/controllers/other_things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/controllers/things_controller.rb +0 -5
- data/fixtures/rails_4_1_8/app/models/.keep +0 -0
- data/fixtures/rails_4_1_8/app/models/concerns/.keep +0 -0
- data/fixtures/rails_4_1_8/app/views/layouts/application.html.erb +0 -11
- data/fixtures/rails_4_1_8/app/views/other_things/index.html.erb +0 -2
- data/fixtures/rails_4_1_8/app/views/things/index.html.erb +0 -1
- data/fixtures/rails_4_1_8/config/application.rb +0 -15
- data/fixtures/rails_4_1_8/config/boot.rb +0 -4
- data/fixtures/rails_4_1_8/config/environment.rb +0 -5
- data/fixtures/rails_4_1_8/config/environments/test.rb +0 -10
- data/fixtures/rails_4_1_8/config/initializers/secure_headers.rb +0 -16
- data/fixtures/rails_4_1_8/config/routes.rb +0 -4
- data/fixtures/rails_4_1_8/config/script_hashes.yml +0 -5
- data/fixtures/rails_4_1_8/config/secrets.yml +0 -22
- data/fixtures/rails_4_1_8/config.ru +0 -4
- data/fixtures/rails_4_1_8/lib/assets/.keep +0 -0
- data/fixtures/rails_4_1_8/lib/tasks/.keep +0 -0
- data/fixtures/rails_4_1_8/log/.keep +0 -0
- data/fixtures/rails_4_1_8/spec/controllers/other_things_controller_spec.rb +0 -83
- data/fixtures/rails_4_1_8/spec/controllers/things_controller_spec.rb +0 -59
- data/fixtures/rails_4_1_8/spec/spec_helper.rb +0 -15
- data/fixtures/rails_4_1_8/vendor/assets/javascripts/.keep +0 -0
- data/fixtures/rails_4_1_8/vendor/assets/stylesheets/.keep +0 -0
- data/lib/secure_headers/controller_extension.rb +0 -158
- data/lib/secure_headers/hash_helper.rb +0 -7
- data/lib/secure_headers/header.rb +0 -5
- data/lib/secure_headers/headers/content_security_policy/script_hash_middleware.rb +0 -22
- data/lib/secure_headers/version.rb +0 -3
- data/lib/tasks/tasks.rake +0 -48
- data/spec/lib/secure_headers/headers/content_security_policy/script_hash_middleware_spec.rb +0 -46
|
@@ -1,334 +1,421 @@
|
|
|
1
1
|
require 'uri'
|
|
2
2
|
require 'base64'
|
|
3
3
|
require 'securerandom'
|
|
4
|
-
require 'user_agent_parser'
|
|
5
4
|
require 'json'
|
|
6
5
|
|
|
7
6
|
module SecureHeaders
|
|
8
|
-
class
|
|
9
|
-
class ContentSecurityPolicy
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
7
|
+
class ContentSecurityPolicyConfigError < StandardError; end
|
|
8
|
+
class ContentSecurityPolicy
|
|
9
|
+
MODERN_BROWSERS = %w(Chrome Opera Firefox)
|
|
10
|
+
DEFAULT_VALUE = "default-src https:".freeze
|
|
11
|
+
DEFAULT_CONFIG = { default_src: %w(https:) }.freeze
|
|
12
|
+
HEADER_NAME = "Content-Security-Policy".freeze
|
|
13
|
+
REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze
|
|
14
|
+
HEADER_NAMES = [HEADER_NAME, REPORT_ONLY]
|
|
15
|
+
DATA_PROTOCOL = "data:".freeze
|
|
16
|
+
SELF = "'self'".freeze
|
|
17
|
+
NONE = "'none'".freeze
|
|
18
|
+
STAR = "*".freeze
|
|
19
|
+
UNSAFE_INLINE = "'unsafe-inline'".freeze
|
|
20
|
+
UNSAFE_EVAL = "'unsafe-eval'".freeze
|
|
21
|
+
|
|
22
|
+
# leftover deprecated values that will be in common use upon upgrading.
|
|
23
|
+
DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze
|
|
24
|
+
|
|
25
|
+
DEFAULT_SRC = :default_src
|
|
26
|
+
CONNECT_SRC = :connect_src
|
|
27
|
+
FONT_SRC = :font_src
|
|
28
|
+
FRAME_SRC = :frame_src
|
|
29
|
+
IMG_SRC = :img_src
|
|
30
|
+
MEDIA_SRC = :media_src
|
|
31
|
+
OBJECT_SRC = :object_src
|
|
32
|
+
SANDBOX = :sandbox
|
|
33
|
+
SCRIPT_SRC = :script_src
|
|
34
|
+
STYLE_SRC = :style_src
|
|
35
|
+
REPORT_URI = :report_uri
|
|
36
|
+
|
|
37
|
+
DIRECTIVES_1_0 = [
|
|
38
|
+
DEFAULT_SRC,
|
|
39
|
+
CONNECT_SRC,
|
|
40
|
+
FONT_SRC,
|
|
41
|
+
FRAME_SRC,
|
|
42
|
+
IMG_SRC,
|
|
43
|
+
MEDIA_SRC,
|
|
44
|
+
OBJECT_SRC,
|
|
45
|
+
SANDBOX,
|
|
46
|
+
SCRIPT_SRC,
|
|
47
|
+
STYLE_SRC,
|
|
48
|
+
REPORT_URI
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
BASE_URI = :base_uri
|
|
52
|
+
CHILD_SRC = :child_src
|
|
53
|
+
FORM_ACTION = :form_action
|
|
54
|
+
FRAME_ANCESTORS = :frame_ancestors
|
|
55
|
+
PLUGIN_TYPES = :plugin_types
|
|
56
|
+
|
|
57
|
+
DIRECTIVES_2_0 = [
|
|
58
|
+
DIRECTIVES_1_0,
|
|
59
|
+
BASE_URI,
|
|
60
|
+
CHILD_SRC,
|
|
61
|
+
FORM_ACTION,
|
|
62
|
+
FRAME_ANCESTORS,
|
|
63
|
+
PLUGIN_TYPES
|
|
64
|
+
].flatten.freeze
|
|
65
|
+
|
|
66
|
+
# All the directives currently under consideration for CSP level 3.
|
|
67
|
+
# https://w3c.github.io/webappsec/specs/CSP2/
|
|
68
|
+
MANIFEST_SRC = :manifest_src
|
|
69
|
+
REFLECTED_XSS = :reflected_xss
|
|
70
|
+
DIRECTIVES_3_0 = [
|
|
71
|
+
DIRECTIVES_2_0,
|
|
72
|
+
MANIFEST_SRC,
|
|
73
|
+
REFLECTED_XSS
|
|
74
|
+
].flatten.freeze
|
|
75
|
+
|
|
76
|
+
# All the directives that are not currently in a formal spec, but have
|
|
77
|
+
# been implemented somewhere.
|
|
78
|
+
BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content
|
|
79
|
+
DIRECTIVES_DRAFT = [
|
|
80
|
+
BLOCK_ALL_MIXED_CONTENT
|
|
81
|
+
].freeze
|
|
82
|
+
|
|
83
|
+
SAFARI_DIRECTIVES = DIRECTIVES_1_0
|
|
84
|
+
|
|
85
|
+
FIREFOX_UNSUPPORTED_DIRECTIVES = [
|
|
86
|
+
BLOCK_ALL_MIXED_CONTENT,
|
|
87
|
+
CHILD_SRC,
|
|
88
|
+
PLUGIN_TYPES
|
|
89
|
+
].freeze
|
|
90
|
+
|
|
91
|
+
FIREFOX_DIRECTIVES = (
|
|
92
|
+
DIRECTIVES_2_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
|
|
93
|
+
).freeze
|
|
94
|
+
|
|
95
|
+
CHROME_DIRECTIVES = (
|
|
96
|
+
DIRECTIVES_2_0 + DIRECTIVES_DRAFT
|
|
97
|
+
).freeze
|
|
98
|
+
|
|
99
|
+
ALL_DIRECTIVES = [DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0 + DIRECTIVES_DRAFT].flatten.uniq.sort
|
|
100
|
+
|
|
101
|
+
# Think of default-src and report-uri as the beginning and end respectively,
|
|
102
|
+
# everything else is in between.
|
|
103
|
+
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
|
|
104
|
+
|
|
105
|
+
VARIATIONS = {
|
|
106
|
+
"Chrome" => CHROME_DIRECTIVES,
|
|
107
|
+
"Opera" => CHROME_DIRECTIVES,
|
|
108
|
+
"Firefox" => FIREFOX_DIRECTIVES,
|
|
109
|
+
"Safari" => SAFARI_DIRECTIVES,
|
|
110
|
+
"Other" => CHROME_DIRECTIVES
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
OTHER = "Other".freeze
|
|
114
|
+
|
|
115
|
+
DIRECTIVE_VALUE_TYPES = {
|
|
116
|
+
BASE_URI => :source_list,
|
|
117
|
+
BLOCK_ALL_MIXED_CONTENT => :boolean,
|
|
118
|
+
CHILD_SRC => :source_list,
|
|
119
|
+
CONNECT_SRC => :source_list,
|
|
120
|
+
DEFAULT_SRC => :source_list,
|
|
121
|
+
FONT_SRC => :source_list,
|
|
122
|
+
FORM_ACTION => :source_list,
|
|
123
|
+
FRAME_ANCESTORS => :source_list,
|
|
124
|
+
FRAME_SRC => :source_list,
|
|
125
|
+
IMG_SRC => :source_list,
|
|
126
|
+
MANIFEST_SRC => :source_list,
|
|
127
|
+
MEDIA_SRC => :source_list,
|
|
128
|
+
OBJECT_SRC => :source_list,
|
|
129
|
+
PLUGIN_TYPES => :source_list,
|
|
130
|
+
REFLECTED_XSS => :string,
|
|
131
|
+
REPORT_URI => :source_list,
|
|
132
|
+
SANDBOX => :string,
|
|
133
|
+
SCRIPT_SRC => :source_list,
|
|
134
|
+
STYLE_SRC => :source_list
|
|
135
|
+
}.freeze
|
|
136
|
+
|
|
137
|
+
CONFIG_KEY = :csp
|
|
138
|
+
STAR_REGEXP = Regexp.new(Regexp.escape(STAR))
|
|
139
|
+
HTTP_SCHEME_REGEX = %r{\Ahttps?://}
|
|
140
|
+
|
|
141
|
+
WILDCARD_SOURCES = [
|
|
142
|
+
UNSAFE_EVAL,
|
|
143
|
+
UNSAFE_INLINE,
|
|
144
|
+
STAR
|
|
145
|
+
]
|
|
78
146
|
|
|
79
147
|
class << self
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def
|
|
85
|
-
|
|
148
|
+
# Public: generate a header name, value array that is user-agent-aware.
|
|
149
|
+
#
|
|
150
|
+
# Returns a default policy if no configuration is provided, or a
|
|
151
|
+
# header name and value based on the config.
|
|
152
|
+
def make_header(config, user_agent)
|
|
153
|
+
header = new(config, user_agent)
|
|
154
|
+
[header.name, header.value]
|
|
86
155
|
end
|
|
87
156
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
# Public: Validates that the configuration has a valid type, or that it is a valid
|
|
158
|
+
# source expression.
|
|
159
|
+
#
|
|
160
|
+
# Private: validates that a source expression:
|
|
161
|
+
# 1. has a valid name
|
|
162
|
+
# 2. is an array of strings
|
|
163
|
+
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
164
|
+
#
|
|
165
|
+
# Does not validate the invididual values of the source expression (e.g.
|
|
166
|
+
# script_src => h*t*t*p: will not raise an exception)
|
|
167
|
+
def validate_config!(config)
|
|
168
|
+
return if config.nil? || config == OPT_OUT
|
|
169
|
+
raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config[:default_src]
|
|
170
|
+
config.each do |key, value|
|
|
171
|
+
if key == :report_only
|
|
172
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil?
|
|
173
|
+
else
|
|
174
|
+
validate_directive!(key, value)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
95
177
|
end
|
|
96
178
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
179
|
+
# Public: combine the values from two different configs.
|
|
180
|
+
#
|
|
181
|
+
# original - the main config
|
|
182
|
+
# additions - values to be merged in
|
|
183
|
+
#
|
|
184
|
+
# raises an error if the original config is OPT_OUT
|
|
185
|
+
#
|
|
186
|
+
# 1. for non-source-list values (report_only, block_all_mixed_content),
|
|
187
|
+
# additions will overwrite the original value.
|
|
188
|
+
# 2. if a value in additions does not exist in the original config, the
|
|
189
|
+
# default-src value is included to match original behavior.
|
|
190
|
+
# 3. if a value in additions does exist in the original config, the two
|
|
191
|
+
# values are joined.
|
|
192
|
+
def combine_policies(original, additions)
|
|
193
|
+
if original == OPT_OUT
|
|
194
|
+
raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.")
|
|
195
|
+
end
|
|
104
196
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
# rails 2/3.0
|
|
111
|
-
request.url
|
|
197
|
+
# in case we would be appending to an empty directive, fill it with the default-src value
|
|
198
|
+
additions.keys.each do |directive|
|
|
199
|
+
unless original[directive] || !source_list?(directive)
|
|
200
|
+
original[directive] = original[:default_src]
|
|
201
|
+
end
|
|
112
202
|
end
|
|
113
|
-
end
|
|
114
203
|
|
|
115
|
-
|
|
116
|
-
|
|
204
|
+
# merge the two hashes. combine (instead of overwrite) the array values
|
|
205
|
+
# when each hash contains a value for a given key.
|
|
206
|
+
original.merge(additions) do |directive, lhs, rhs|
|
|
207
|
+
if source_list?(directive)
|
|
208
|
+
lhs | rhs
|
|
209
|
+
else
|
|
210
|
+
rhs
|
|
211
|
+
end
|
|
212
|
+
end
|
|
117
213
|
end
|
|
118
|
-
end
|
|
119
214
|
|
|
120
|
-
|
|
121
|
-
# :controller used for setting instance variables for nonces/hashes
|
|
122
|
-
# :ssl_request used to determine if http_additions should be used
|
|
123
|
-
# :ua the user agent (or just use Firefox/Chrome/MSIE/etc)
|
|
124
|
-
#
|
|
125
|
-
# :report used to determine what :ssl_request, :ua, and :request_uri are set to
|
|
126
|
-
def initialize(config=nil, options={})
|
|
127
|
-
return unless config
|
|
215
|
+
private
|
|
128
216
|
|
|
129
|
-
|
|
130
|
-
|
|
217
|
+
def source_list?(directive)
|
|
218
|
+
DIRECTIVE_VALUE_TYPES[directive] == :source_list
|
|
131
219
|
end
|
|
132
220
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
config_val = if value.respond_to?(:call)
|
|
141
|
-
warn "[DEPRECATION] secure_headers 3.x will not support procs as config values."
|
|
142
|
-
value.call(@controller)
|
|
143
|
-
else
|
|
144
|
-
value
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
if ALL_DIRECTIVES.include?(key.to_sym) # directives need to be normalized to arrays of strings
|
|
148
|
-
if config_val.is_a? String
|
|
149
|
-
warn "[DEPRECATION] A String was supplied for directive #{key}. secure_headers 3.x will require all directives to be arrays of strings."
|
|
150
|
-
config_val = config_val.split
|
|
221
|
+
# Private: Validates that the configuration has a valid type, or that it is a valid
|
|
222
|
+
# source expression.
|
|
223
|
+
def validate_directive!(key, value)
|
|
224
|
+
case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[key]
|
|
225
|
+
when :boolean
|
|
226
|
+
unless boolean?(value)
|
|
227
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value")
|
|
151
228
|
end
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end.flatten.uniq
|
|
229
|
+
when :string
|
|
230
|
+
unless value.is_a?(String)
|
|
231
|
+
raise ContentSecurityPolicyConfigError.new("#{key} Must be a string. Found #{config.class}: #{config} value")
|
|
156
232
|
end
|
|
233
|
+
else
|
|
234
|
+
validate_source_expression!(key, value)
|
|
157
235
|
end
|
|
158
|
-
|
|
159
|
-
hash[key] = config_val
|
|
160
|
-
hash
|
|
161
236
|
end
|
|
162
237
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"https:" + report_uri
|
|
179
|
-
else
|
|
180
|
-
"http:" + report_uri
|
|
181
|
-
end
|
|
182
|
-
end
|
|
238
|
+
# Private: validates that a source expression:
|
|
239
|
+
# 1. has a valid name
|
|
240
|
+
# 2. is an array of strings
|
|
241
|
+
# 3. does not contain any depreated, now invalid values (inline, eval, self, none)
|
|
242
|
+
#
|
|
243
|
+
# Does not validate the invididual values of the source expression (e.g.
|
|
244
|
+
# script_src => h*t*t*p: will not raise an exception)
|
|
245
|
+
def validate_source_expression!(key, value)
|
|
246
|
+
# source expressions
|
|
247
|
+
unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(key)
|
|
248
|
+
raise ContentSecurityPolicyConfigError.new("Unknown directive #{key}")
|
|
249
|
+
end
|
|
250
|
+
unless value.is_a?(Array) && value.all? { |v| v.is_a?(String) }
|
|
251
|
+
raise ContentSecurityPolicyConfigError.new("#{key} must be an array of strings")
|
|
252
|
+
end
|
|
183
253
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
254
|
+
value.each do |source_expression|
|
|
255
|
+
if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(source_expression)
|
|
256
|
+
raise ContentSecurityPolicyConfigError.new("#{key} contains an invalid keyword source (#{source_expression}). This value must be single quoted.")
|
|
187
257
|
end
|
|
188
|
-
report_uri
|
|
189
258
|
end
|
|
190
259
|
end
|
|
191
260
|
|
|
192
|
-
|
|
193
|
-
|
|
261
|
+
def boolean?(value)
|
|
262
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
263
|
+
end
|
|
194
264
|
end
|
|
195
265
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
266
|
+
|
|
267
|
+
def initialize(config = nil, user_agent = OTHER)
|
|
268
|
+
config = Configuration.deep_copy(DEFAULT_CONFIG) unless config
|
|
269
|
+
@config = config
|
|
270
|
+
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
|
|
271
|
+
user_agent
|
|
272
|
+
else
|
|
273
|
+
UserAgent.parse(user_agent)
|
|
274
|
+
end
|
|
275
|
+
@report_only = !!@config[:report_only]
|
|
276
|
+
@script_nonce = @config[:script_nonce]
|
|
277
|
+
@style_nonce = @config[:style_nonce]
|
|
202
278
|
end
|
|
203
279
|
|
|
204
280
|
##
|
|
205
281
|
# Returns the name to use for the header. Either "Content-Security-Policy" or
|
|
206
282
|
# "Content-Security-Policy-Report-Only"
|
|
207
283
|
def name
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
284
|
+
if @report_only
|
|
285
|
+
REPORT_ONLY
|
|
286
|
+
else
|
|
287
|
+
HEADER_NAME
|
|
211
288
|
end
|
|
212
|
-
base
|
|
213
289
|
end
|
|
214
290
|
|
|
215
291
|
##
|
|
216
292
|
# Return the value of the CSP header
|
|
217
293
|
def value
|
|
218
|
-
|
|
219
|
-
if @config
|
|
294
|
+
@value ||= if @config
|
|
220
295
|
build_value
|
|
221
296
|
else
|
|
222
|
-
|
|
297
|
+
DEFAULT_VALUE
|
|
223
298
|
end
|
|
224
299
|
end
|
|
225
300
|
|
|
226
|
-
|
|
227
|
-
build_value
|
|
228
|
-
@config.inject({}) do |hash, (key, value)|
|
|
229
|
-
if ALL_DIRECTIVES.include?(key)
|
|
230
|
-
hash[key.to_s.gsub(/(\w+)_(\w+)/, "\\1-\\2")] = value
|
|
231
|
-
end
|
|
232
|
-
hash
|
|
233
|
-
end.to_json
|
|
234
|
-
end
|
|
301
|
+
private
|
|
235
302
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
303
|
+
# Private: converts the config object into a string representing a policy.
|
|
304
|
+
# Places default-src at the first directive and report-uri as the last. All
|
|
305
|
+
# others are presented in alphabetical order.
|
|
306
|
+
#
|
|
307
|
+
# Unsupported directives are filtered based on the user agent.
|
|
308
|
+
#
|
|
309
|
+
# Returns a content security policy header value.
|
|
310
|
+
def build_value
|
|
311
|
+
directives.map do |directive_name|
|
|
312
|
+
case DIRECTIVE_VALUE_TYPES[directive_name]
|
|
313
|
+
when :boolean
|
|
314
|
+
symbol_to_hyphen_case(directive_name)
|
|
315
|
+
when :string
|
|
316
|
+
[symbol_to_hyphen_case(directive_name), @config[directive_name]].join(" ")
|
|
317
|
+
else
|
|
318
|
+
build_directive(directive_name)
|
|
244
319
|
end
|
|
245
|
-
end
|
|
320
|
+
end.join("; ")
|
|
246
321
|
end
|
|
247
322
|
|
|
248
|
-
|
|
323
|
+
# Private: builds a string that represents one directive in a minified form.
|
|
324
|
+
# If a directive contains *, all other values are omitted.
|
|
325
|
+
# If a directive contains 'none' but has other values, 'none' is ommitted.
|
|
326
|
+
# Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression)
|
|
327
|
+
#
|
|
328
|
+
# directive_name - a symbol representing the various ALL_DIRECTIVES
|
|
329
|
+
#
|
|
330
|
+
# Returns a string representing a directive.
|
|
331
|
+
def build_directive(directive_name)
|
|
332
|
+
source_list = @config[directive_name].compact
|
|
249
333
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
334
|
+
value = if source_list.include?(STAR)
|
|
335
|
+
# Discard trailing entries (excluding unsafe-*) since * accomplishes the same.
|
|
336
|
+
source_list.select { |value| WILDCARD_SOURCES.include?(value) }
|
|
337
|
+
else
|
|
338
|
+
populate_nonces(directive_name, source_list)
|
|
253
339
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
append_http_additions unless ssl_request?
|
|
257
|
-
generic_directives
|
|
258
|
-
end
|
|
340
|
+
# Discard any 'none' values if more directives are supplied since none may override values.
|
|
341
|
+
source_list.reject! { |value| value == NONE } if source_list.length > 1
|
|
259
342
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
@config[k] ||= []
|
|
264
|
-
@config[k] << v
|
|
343
|
+
# remove schemes and dedup source expressions
|
|
344
|
+
source_list = strip_source_schemes(source_list) unless directive_name == REPORT_URI
|
|
345
|
+
dedup_source_list(source_list).join(" ")
|
|
265
346
|
end
|
|
347
|
+
[symbol_to_hyphen_case(directive_name), value].join(" ")
|
|
266
348
|
end
|
|
267
349
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
else
|
|
280
|
-
"'unsafe-inline'"
|
|
350
|
+
# Removes duplicates and sources that already match an existing wild card.
|
|
351
|
+
#
|
|
352
|
+
# e.g. *.github.com asdf.github.com becomes *.github.com
|
|
353
|
+
def dedup_source_list(sources)
|
|
354
|
+
sources = sources.uniq
|
|
355
|
+
wild_sources = sources.select { |source| source =~ STAR_REGEXP }
|
|
356
|
+
|
|
357
|
+
if wild_sources.any?
|
|
358
|
+
sources.reject do |source|
|
|
359
|
+
!wild_sources.include?(source) &&
|
|
360
|
+
wild_sources.any? { |pattern| File.fnmatch(pattern, source) }
|
|
281
361
|
end
|
|
282
362
|
else
|
|
283
|
-
|
|
363
|
+
sources
|
|
284
364
|
end
|
|
285
365
|
end
|
|
286
366
|
|
|
287
|
-
#
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
367
|
+
# Private: append a nonce to the script/style directories if script_nonce
|
|
368
|
+
# or style_nonce are provided.
|
|
369
|
+
def populate_nonces(directive, source_list)
|
|
370
|
+
case directive
|
|
371
|
+
when SCRIPT_SRC
|
|
372
|
+
append_nonce(source_list, @script_nonce)
|
|
373
|
+
when STYLE_SRC
|
|
374
|
+
append_nonce(source_list, @style_nonce)
|
|
295
375
|
end
|
|
376
|
+
end
|
|
296
377
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
378
|
+
# Private: adds a nonce or 'unsafe-inline' depending on browser support.
|
|
379
|
+
# If a nonce is populated, inline content is assumed.
|
|
380
|
+
#
|
|
381
|
+
# While CSP is backward compatible in that a policy with a nonce will ignore
|
|
382
|
+
# unsafe-inline, this is more concise.
|
|
383
|
+
def append_nonce(source_list, nonce)
|
|
384
|
+
if nonce
|
|
385
|
+
if nonces_supported?
|
|
386
|
+
source_list << "'nonce-#{nonce}'"
|
|
387
|
+
else
|
|
388
|
+
source_list << UNSAFE_INLINE
|
|
300
389
|
end
|
|
301
390
|
end
|
|
302
|
-
|
|
303
|
-
header_value += build_directive(:report_uri) if @config[:report_uri]
|
|
304
|
-
|
|
305
|
-
header_value.strip
|
|
306
391
|
end
|
|
307
392
|
|
|
308
|
-
|
|
309
|
-
|
|
393
|
+
# Private: return the list of directives that are supported by the user agent,
|
|
394
|
+
# starting with default-src and ending with report-uri.
|
|
395
|
+
def directives
|
|
396
|
+
[DEFAULT_SRC,
|
|
397
|
+
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
|
|
398
|
+
REPORT_URI].flatten.select { |directive| @config.key?(directive) }
|
|
310
399
|
end
|
|
311
400
|
|
|
312
|
-
|
|
313
|
-
|
|
401
|
+
# Private: Remove scheme from source expressions.
|
|
402
|
+
def strip_source_schemes(source_list)
|
|
403
|
+
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
|
|
314
404
|
end
|
|
315
405
|
|
|
406
|
+
# Private: determine which directives are supported for the given user agent.
|
|
407
|
+
#
|
|
408
|
+
# Returns an array of symbols representing the directives.
|
|
316
409
|
def supported_directives
|
|
317
|
-
@supported_directives ||=
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
when "Firefox"
|
|
323
|
-
FIREFOX_DIRECTIVES
|
|
324
|
-
else
|
|
325
|
-
DIRECTIVES_1_0
|
|
326
|
-
end
|
|
410
|
+
@supported_directives ||= VARIATIONS[@parsed_ua.browser] || VARIATIONS[OTHER]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def nonces_supported?
|
|
414
|
+
@nonces_supported ||= MODERN_BROWSERS.include?(@parsed_ua.browser)
|
|
327
415
|
end
|
|
328
416
|
|
|
329
|
-
def
|
|
330
|
-
|
|
331
|
-
["Chrome", "Opera", "Firefox"].include?(parsed_ua.family)
|
|
417
|
+
def symbol_to_hyphen_case(sym)
|
|
418
|
+
sym.to_s.tr('_', '-')
|
|
332
419
|
end
|
|
333
420
|
end
|
|
334
421
|
end
|