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,61 +1,45 @@
|
|
|
1
1
|
module SecureHeaders
|
|
2
|
-
class UnexpectedHashedScriptException < StandardError
|
|
3
|
-
|
|
4
|
-
end
|
|
5
|
-
|
|
6
2
|
module ViewHelpers
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
# Public: create a style tag using the content security policy nonce.
|
|
4
|
+
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
5
|
+
#
|
|
6
|
+
# Returns an html-safe style tag with the nonce attribute.
|
|
7
|
+
def nonced_style_tag(content_or_options = nil, &block)
|
|
8
|
+
nonced_tag(:style, content_or_options, block)
|
|
12
9
|
end
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
# Public: create a script tag using the content security policy nonce.
|
|
12
|
+
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
13
|
+
#
|
|
14
|
+
# Returns an html-safe script tag with the nonce attribute.
|
|
15
|
+
def nonced_javascript_tag(content_or_options = nil, &block)
|
|
16
|
+
nonced_tag(:script, content_or_options, block)
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
raise UnexpectedHashedScriptException.new(message)
|
|
29
|
-
else
|
|
30
|
-
request.env[HASHES_ENV_KEY] = (request.env[HASHES_ENV_KEY] || []) << hash_value
|
|
31
|
-
end
|
|
32
|
-
end
|
|
19
|
+
# Public: use the content security policy nonce for this request directly.
|
|
20
|
+
# Instructs secure_headers to append a nonce to style/script-src directives.
|
|
21
|
+
#
|
|
22
|
+
# Returns a non-html-safe nonce value.
|
|
23
|
+
def content_security_policy_nonce(type)
|
|
24
|
+
case type
|
|
25
|
+
when :script
|
|
26
|
+
SecureHeaders.content_security_policy_script_nonce(@_request)
|
|
27
|
+
when :style
|
|
28
|
+
SecureHeaders.content_security_policy_style_nonce(@_request)
|
|
33
29
|
end
|
|
34
|
-
|
|
35
|
-
content_tag :script, content
|
|
36
30
|
end
|
|
37
31
|
|
|
38
32
|
private
|
|
39
33
|
|
|
40
|
-
def nonced_tag(
|
|
34
|
+
def nonced_tag(type, content_or_options, block)
|
|
35
|
+
options = {}
|
|
41
36
|
content = if block
|
|
37
|
+
options = content_or_options
|
|
42
38
|
capture(&block)
|
|
43
39
|
else
|
|
44
|
-
|
|
40
|
+
content_or_options.html_safe # :'(
|
|
45
41
|
end
|
|
46
|
-
|
|
47
|
-
content_tag type, content, :nonce => @content_security_policy_nonce
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def unexpected_hash_error_message(file_path, hash_value, content)
|
|
51
|
-
<<-EOF
|
|
52
|
-
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
|
|
53
|
-
<script>#{content}</script>
|
|
54
|
-
*** This is fine in dev/test, but will raise exceptions in production. ***
|
|
55
|
-
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/script_hashes.yml:***
|
|
56
|
-
#{file_path}:
|
|
57
|
-
- #{hash_value}\n\n
|
|
58
|
-
EOF
|
|
42
|
+
content_tag type, content, options.merge(nonce: content_security_policy_nonce(type))
|
|
59
43
|
end
|
|
60
44
|
end
|
|
61
45
|
end
|
data/lib/secure_headers.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
require "secure_headers/
|
|
2
|
-
require "secure_headers/header"
|
|
1
|
+
require "secure_headers/configuration"
|
|
3
2
|
require "secure_headers/headers/public_key_pins"
|
|
4
3
|
require "secure_headers/headers/content_security_policy"
|
|
5
4
|
require "secure_headers/headers/x_frame_options"
|
|
@@ -8,86 +7,280 @@ require "secure_headers/headers/x_xss_protection"
|
|
|
8
7
|
require "secure_headers/headers/x_content_type_options"
|
|
9
8
|
require "secure_headers/headers/x_download_options"
|
|
10
9
|
require "secure_headers/headers/x_permitted_cross_domain_policies"
|
|
11
|
-
require "secure_headers/
|
|
10
|
+
require "secure_headers/middleware"
|
|
12
11
|
require "secure_headers/railtie"
|
|
13
|
-
require "secure_headers/hash_helper"
|
|
14
12
|
require "secure_headers/view_helper"
|
|
13
|
+
require "useragent"
|
|
15
14
|
|
|
15
|
+
# All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT
|
|
16
|
+
# or ":optout_of_protection" as a config value to disable a given header
|
|
16
17
|
module SecureHeaders
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
OPT_OUT = :opt_out_of_protection
|
|
19
|
+
SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze
|
|
20
|
+
NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze
|
|
21
|
+
HTTPS = "https".freeze
|
|
22
|
+
CSP = ContentSecurityPolicy
|
|
19
23
|
|
|
20
24
|
ALL_HEADER_CLASSES = [
|
|
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
|
-
|
|
25
|
+
ContentSecurityPolicy,
|
|
26
|
+
StrictTransportSecurity,
|
|
27
|
+
PublicKeyPins,
|
|
28
|
+
XContentTypeOptions,
|
|
29
|
+
XDownloadOptions,
|
|
30
|
+
XFrameOptions,
|
|
31
|
+
XPermittedCrossDomainPolicies,
|
|
32
|
+
XXssProtection
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
ALL_HEADERS_BESIDES_CSP = (ALL_HEADER_CLASSES - [CSP]).freeze
|
|
36
|
+
|
|
37
|
+
# Headers set on http requests (excludes STS and HPKP)
|
|
38
|
+
HTTP_HEADER_CLASSES =
|
|
39
|
+
(ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze
|
|
40
|
+
|
|
41
|
+
class << self
|
|
42
|
+
# Public: override a given set of directives for the current request. If a
|
|
43
|
+
# value already exists for a given directive, it will be overridden.
|
|
44
|
+
#
|
|
45
|
+
# If CSP was previously OPT_OUT, a new blank policy is used.
|
|
46
|
+
#
|
|
47
|
+
# additions - a hash containing directives. e.g.
|
|
48
|
+
# script_src: %w(another-host.com)
|
|
49
|
+
def override_content_security_policy_directives(request, additions)
|
|
50
|
+
config = config_for(request).dup
|
|
51
|
+
if config.csp == OPT_OUT
|
|
52
|
+
config.csp = {}
|
|
53
|
+
end
|
|
54
|
+
config.csp.merge!(additions)
|
|
55
|
+
override_secure_headers_request_config(request, config)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Public: appends source values to the current configuration. If no value
|
|
59
|
+
# is set for a given directive, the value will be merged with the default-src
|
|
60
|
+
# value. If a value exists for the given directive, the values will be combined.
|
|
61
|
+
#
|
|
62
|
+
# additions - a hash containing directives. e.g.
|
|
63
|
+
# script_src: %w(another-host.com)
|
|
64
|
+
def append_content_security_policy_directives(request, additions)
|
|
65
|
+
config = config_for(request).dup
|
|
66
|
+
config.csp = CSP.combine_policies(config.csp, additions)
|
|
67
|
+
override_secure_headers_request_config(request, config)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Public: override X-Frame-Options settings for this request.
|
|
71
|
+
#
|
|
72
|
+
# value - deny, sameorigin, or allowall
|
|
73
|
+
#
|
|
74
|
+
# Returns the current config
|
|
75
|
+
def override_x_frame_options(request, value)
|
|
76
|
+
default_config = config_for(request).dup
|
|
77
|
+
default_config.x_frame_options = value
|
|
78
|
+
override_secure_headers_request_config(request, default_config)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Public: opts out of setting a given header by creating a temporary config
|
|
82
|
+
# and setting the given headers config to OPT_OUT.
|
|
83
|
+
def opt_out_of_header(request, header_key)
|
|
84
|
+
config = config_for(request).dup
|
|
85
|
+
config.send("#{header_key}=", OPT_OUT)
|
|
86
|
+
override_secure_headers_request_config(request, config)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Public: opts out of setting all headers by telling secure_headers to use
|
|
90
|
+
# the NOOP configuration.
|
|
91
|
+
def opt_out_of_all_protection(request)
|
|
92
|
+
use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Public: Builds the hash of headers that should be applied base on the
|
|
96
|
+
# request.
|
|
97
|
+
#
|
|
98
|
+
# StrictTransportSecurity and PublicKeyPins are not applied to http requests.
|
|
99
|
+
# See #config_for to determine which config is used for a given request.
|
|
100
|
+
#
|
|
101
|
+
# Returns a hash of header names => header values. The value
|
|
102
|
+
# returned is meant to be merged into the header value from `@app.call(env)`
|
|
103
|
+
# in Rack middleware.
|
|
104
|
+
def header_hash_for(request)
|
|
105
|
+
config = config_for(request)
|
|
106
|
+
|
|
107
|
+
headers = if cached_headers = config.cached_headers
|
|
108
|
+
use_cached_headers(cached_headers, request)
|
|
109
|
+
else
|
|
110
|
+
build_headers(config, request)
|
|
55
111
|
end
|
|
56
112
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
113
|
+
headers
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Public: specify which named override will be used for this request.
|
|
117
|
+
# Raises an argument error if no named override exists.
|
|
118
|
+
#
|
|
119
|
+
# name - the name of the previously configured override.
|
|
120
|
+
def use_secure_headers_override(request, name)
|
|
121
|
+
if config = Configuration.get(name)
|
|
122
|
+
override_secure_headers_request_config(request, config)
|
|
123
|
+
else
|
|
124
|
+
raise ArgumentError.new("no override by the name of #{name} has been configured")
|
|
60
125
|
end
|
|
61
126
|
end
|
|
62
|
-
end
|
|
63
127
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
128
|
+
# Public: gets or creates a nonce for CSP.
|
|
129
|
+
#
|
|
130
|
+
# The nonce will be added to script_src
|
|
131
|
+
#
|
|
132
|
+
# Returns the nonce
|
|
133
|
+
def content_security_policy_script_nonce(request)
|
|
134
|
+
content_security_policy_nonce(request, CSP::SCRIPT_SRC)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Public: gets or creates a nonce for CSP.
|
|
138
|
+
#
|
|
139
|
+
# The nonce will be added to style_src
|
|
140
|
+
#
|
|
141
|
+
# Returns the nonce
|
|
142
|
+
def content_security_policy_style_nonce(request)
|
|
143
|
+
content_security_policy_nonce(request, CSP::STYLE_SRC)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Private: gets or creates a nonce for CSP.
|
|
149
|
+
#
|
|
150
|
+
# Returns the nonce
|
|
151
|
+
def content_security_policy_nonce(request, script_or_style)
|
|
152
|
+
request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp
|
|
153
|
+
nonce_key = script_or_style == CSP::SCRIPT_SRC ? :script_nonce : :style_nonce
|
|
154
|
+
append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY])
|
|
155
|
+
request.env[NONCE_KEY]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Private: convenience method for specifying which configuration object should
|
|
159
|
+
# be used for this request.
|
|
160
|
+
#
|
|
161
|
+
# Returns the config.
|
|
162
|
+
def override_secure_headers_request_config(request, config)
|
|
163
|
+
request.env[SECURE_HEADERS_CONFIG] = config
|
|
67
164
|
end
|
|
68
165
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
166
|
+
# Private: determines which headers are applicable to a given request.
|
|
167
|
+
#
|
|
168
|
+
# Returns a list of classes whose corresponding header values are valid for
|
|
169
|
+
# this request.
|
|
170
|
+
def header_classes_for(request)
|
|
171
|
+
if request.scheme == HTTPS
|
|
172
|
+
ALL_HEADER_CLASSES
|
|
173
|
+
else
|
|
174
|
+
HTTP_HEADER_CLASSES
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Private: do the heavy lifting of converting a configuration object
|
|
179
|
+
# to a hash of headers valid for this request.
|
|
180
|
+
#
|
|
181
|
+
# Returns a hash of header names / values.
|
|
182
|
+
def build_headers(config, request)
|
|
183
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
184
|
+
header_config = if config
|
|
185
|
+
config.fetch(klass::CONFIG_KEY)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
header_name, value = if klass == CSP
|
|
189
|
+
make_header(klass, header_config, request.user_agent)
|
|
75
190
|
else
|
|
76
|
-
|
|
191
|
+
make_header(klass, header_config)
|
|
77
192
|
end
|
|
193
|
+
hash[header_name] = value if value
|
|
194
|
+
end
|
|
195
|
+
end
|
|
78
196
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
197
|
+
# Private: takes a precomputed hash of headers and returns the Headers
|
|
198
|
+
# customized for the request.
|
|
199
|
+
#
|
|
200
|
+
# Returns a hash of header names / values valid for a given request.
|
|
201
|
+
def use_cached_headers(default_headers, request)
|
|
202
|
+
header_classes_for(request).each_with_object({}) do |klass, hash|
|
|
203
|
+
if default_header = default_headers[klass::CONFIG_KEY]
|
|
204
|
+
header_name, value = if klass == CSP
|
|
205
|
+
default_csp_header_for_ua(default_header, request)
|
|
206
|
+
else
|
|
207
|
+
default_header
|
|
208
|
+
end
|
|
209
|
+
hash[header_name] = value
|
|
82
210
|
end
|
|
83
|
-
memo
|
|
84
211
|
end
|
|
85
212
|
end
|
|
86
213
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
214
|
+
# Private: Retreives the config for a given header type:
|
|
215
|
+
#
|
|
216
|
+
# Checks to see if there is an override for this request, then
|
|
217
|
+
# Checks to see if a named override is used for this request, then
|
|
218
|
+
# Falls back to the global config
|
|
219
|
+
def config_for(request)
|
|
220
|
+
request.env[SECURE_HEADERS_CONFIG] ||
|
|
221
|
+
Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Private: chooses the applicable CSP header for the provided user agent.
|
|
225
|
+
#
|
|
226
|
+
# headers - a hash of header_config_key => [header_name, header_value]
|
|
227
|
+
#
|
|
228
|
+
# Returns a CSP [header, value] array
|
|
229
|
+
def default_csp_header_for_ua(headers, request)
|
|
230
|
+
family = UserAgent.parse(request.user_agent).browser
|
|
231
|
+
if CSP::VARIATIONS.key?(family)
|
|
232
|
+
headers[family]
|
|
233
|
+
else
|
|
234
|
+
headers[CSP::OTHER]
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Private: optionally build a header with a given configure
|
|
239
|
+
#
|
|
240
|
+
# klass - corresponding Class for a given header
|
|
241
|
+
# config - A string, symbol, or hash config for the header
|
|
242
|
+
# user_agent - A string representing the UA (only used for CSP feature sniffing)
|
|
243
|
+
#
|
|
244
|
+
# Returns a 2 element array [header_name, header_value] or nil if config
|
|
245
|
+
# is OPT_OUT
|
|
246
|
+
def make_header(klass, header_config, user_agent = nil)
|
|
247
|
+
unless header_config == OPT_OUT
|
|
248
|
+
if klass == CSP
|
|
249
|
+
klass.make_header(header_config, user_agent)
|
|
250
|
+
else
|
|
251
|
+
klass.make_header(header_config)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
90
254
|
end
|
|
91
255
|
end
|
|
92
256
|
|
|
257
|
+
# These methods are mixed into controllers and delegate to the class method
|
|
258
|
+
# with the same name.
|
|
259
|
+
def use_secure_headers_override(name)
|
|
260
|
+
SecureHeaders.use_secure_headers_override(request, name)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def content_security_policy_script_nonce
|
|
264
|
+
SecureHeaders.content_security_policy_script_nonce(request)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def content_security_policy_style_nonce
|
|
268
|
+
SecureHeaders.content_security_policy_style_nonce(request)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def opt_out_of_header(header_key)
|
|
272
|
+
SecureHeaders.opt_out_of_header(request, header_key)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def append_content_security_policy_directives(additions)
|
|
276
|
+
SecureHeaders.append_content_security_policy_directives(request, additions)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def override_content_security_policy_directives(additions)
|
|
280
|
+
SecureHeaders.override_content_security_policy_directives(request, additions)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def override_x_frame_options(value)
|
|
284
|
+
SecureHeaders.override_x_frame_options(request, value)
|
|
285
|
+
end
|
|
93
286
|
end
|
data/secure_headers.gemspec
CHANGED
|
@@ -1,24 +1,19 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
-
require 'secure_headers/version'
|
|
5
|
-
|
|
6
2
|
Gem::Specification.new do |gem|
|
|
7
3
|
gem.name = "secure_headers"
|
|
8
|
-
gem.version =
|
|
4
|
+
gem.version = "3.0.0.pre"
|
|
9
5
|
gem.authors = ["Neil Matatall"]
|
|
10
6
|
gem.email = ["neil.matatall@gmail.com"]
|
|
11
|
-
gem.description =
|
|
12
|
-
gem.summary =
|
|
7
|
+
gem.description = 'Security related headers all in one gem.'
|
|
8
|
+
gem.summary = 'Add easily configured security headers to responses
|
|
13
9
|
including content-security-policy, x-frame-options,
|
|
14
|
-
strict-transport-security, etc.
|
|
10
|
+
strict-transport-security, etc.'
|
|
15
11
|
gem.homepage = "https://github.com/twitter/secureheaders"
|
|
16
12
|
gem.license = "Apache Public License 2.0"
|
|
17
|
-
gem.files = `git ls-files`.split(
|
|
18
|
-
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
13
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
|
19
15
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
20
16
|
gem.require_paths = ["lib"]
|
|
21
17
|
gem.add_development_dependency "rake"
|
|
22
|
-
gem.add_dependency "
|
|
23
|
-
gem.post_install_message = "[DEPRECATION] Development has stopped on the 2.x line of secure_headers. It will be maintained, but new features will be added to the 3.x branch. A lot has changed in secure_headers 3.x. A migration guide can be found in the documentation: https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md."
|
|
18
|
+
gem.add_dependency "useragent"
|
|
24
19
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
module SecureHeaders
|
|
4
|
+
describe Configuration do
|
|
5
|
+
before(:each) do
|
|
6
|
+
reset_config
|
|
7
|
+
Configuration.default
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
it "has a default config" do
|
|
11
|
+
expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it "has an 'noop' config" do
|
|
15
|
+
expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "precomputes headers upon creation" do
|
|
19
|
+
default_config = Configuration.get(Configuration::DEFAULT_CONFIG)
|
|
20
|
+
header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash|
|
|
21
|
+
header_name, header_value = if key == :csp
|
|
22
|
+
value["Chrome"]
|
|
23
|
+
else
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
hash[header_name] = header_value
|
|
28
|
+
end
|
|
29
|
+
expect_default_values(header_hash)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "copies all config values except for the cached headers when dup" do
|
|
33
|
+
Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do
|
|
34
|
+
# do nothing, just copy it
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
config = Configuration.get(:test_override)
|
|
38
|
+
noop = Configuration.get(Configuration::NOOP_CONFIGURATION)
|
|
39
|
+
[:hsts, :x_frame_options, :x_content_type_options, :x_xss_protection,
|
|
40
|
+
:x_download_options, :x_permitted_cross_domain_policies, :hpkp, :csp].each do |key|
|
|
41
|
+
|
|
42
|
+
expect(config.send(key)).to eq(noop.send(key)), "Value not copied: #{key}."
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "stores an override of the global config" do
|
|
47
|
+
Configuration.override(:test_override) do |config|
|
|
48
|
+
config.x_frame_options = "DENY"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
expect(Configuration.get(:test_override)).to_not be_nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "deep dup's config values when overriding so the original cannot be modified" do
|
|
55
|
+
Configuration.override(:override) do |config|
|
|
56
|
+
config.csp[:default_src] << "'self'"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
default = Configuration.get
|
|
60
|
+
override = Configuration.get(:override)
|
|
61
|
+
|
|
62
|
+
expect(override.csp).not_to eq(default.csp)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "allows you to override an override" do
|
|
66
|
+
Configuration.override(:override) do |config|
|
|
67
|
+
config.csp = { default_src: %w('self')}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
Configuration.override(:second_override, :override) do |config|
|
|
71
|
+
config.csp = config.csp.merge(script_src: %w(example.org))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
original_override = Configuration.get(:override)
|
|
75
|
+
expect(original_override.csp).to eq(default_src: %w('self'))
|
|
76
|
+
override_config = Configuration.get(:second_override)
|
|
77
|
+
expect(override_config.csp).to eq(default_src: %w('self'), script_src: %w(example.org))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|