Skip to content

Commit 64c0acb

Browse files
committed
Remove all useragent sniffing
1 parent 8378f13 commit 64c0acb

File tree

8 files changed

+13
-215
lines changed

8 files changed

+13
-215
lines changed

lib/secure_headers.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
require "secure_headers/middleware"
1616
require "secure_headers/railtie"
1717
require "secure_headers/view_helper"
18-
require "useragent"
1918
require "singleton"
2019
require "secure_headers/configuration"
2120

@@ -149,8 +148,7 @@ def header_hash_for(request)
149148
prevent_dup = true
150149
config = config_for(request, prevent_dup)
151150
config.validate_config!
152-
user_agent = UserAgent.parse(request.user_agent)
153-
headers = config.generate_headers(user_agent)
151+
headers = config.generate_headers
154152

155153
if request.scheme != HTTPS
156154
HTTPS_HEADER_CLASSES.each do |klass|

lib/secure_headers/configuration.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,11 @@ def override(name = nil, &block)
194194
self
195195
end
196196

197-
def generate_headers(user_agent)
197+
def generate_headers
198198
headers = {}
199199
HEADERABLE_ATTRIBUTES.each do |attr|
200200
klass = CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
201-
header_name, value = klass.make_header(instance_variable_get("@#{attr}"), user_agent)
201+
header_name, value = klass.make_header(instance_variable_get("@#{attr}"))
202202
if header_name && value
203203
headers[header_name] = value
204204
end

lib/secure_headers/headers/content_security_policy.rb

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
# frozen_string_literal: true
22
require_relative "policy_management"
33
require_relative "content_security_policy_config"
4-
require "useragent"
54

65
module SecureHeaders
76
class ContentSecurityPolicy
87
include PolicyManagement
98

10-
# constants to be used for version-specific UA sniffing
11-
VERSION_46 = ::UserAgent::Version.new("46")
12-
VERSION_10 = ::UserAgent::Version.new("10")
13-
FALLBACK_VERSION = ::UserAgent::Version.new("0")
14-
15-
def initialize(config = nil, user_agent = OTHER)
16-
user_agent ||= OTHER
9+
def initialize(config = nil)
1710
@config = if config.is_a?(Hash)
1811
if config[:report_only]
1912
ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG)
@@ -26,12 +19,6 @@ def initialize(config = nil, user_agent = OTHER)
2619
config
2720
end
2821

29-
@parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base)
30-
user_agent
31-
else
32-
UserAgent.parse(user_agent)
33-
end
34-
@frame_src = normalize_child_frame_src
3522
@preserve_schemes = @config.preserve_schemes
3623
@script_nonce = @config.script_nonce
3724
@style_nonce = @config.style_nonce
@@ -56,20 +43,10 @@ def value
5643

5744
private
5845

59-
def normalize_child_frame_src
60-
if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src
61-
raise ArgumentError, "#{Kernel.caller.first}: both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers."
62-
end
63-
64-
@config.frame_src || @config.child_src
65-
end
66-
6746
# Private: converts the config object into a string representing a policy.
6847
# Places default-src at the first directive and report-uri as the last. All
6948
# others are presented in alphabetical order.
7049
#
71-
# Unsupported directives are filtered based on the user agent.
72-
#
7350
# Returns a content security policy header value.
7451
def build_value
7552
directives.map do |directive_name|
@@ -125,18 +102,7 @@ def build_media_type_list_directive(directive)
125102
#
126103
# Returns a string representing a directive.
127104
def build_source_list_directive(directive)
128-
source_list = case directive
129-
when :child_src
130-
if supported_directives.include?(:child_src)
131-
@frame_src
132-
end
133-
when :frame_src
134-
unless supported_directives.include?(:child_src)
135-
@frame_src
136-
end
137-
else
138-
@config.directive_value(directive)
139-
end
105+
source_list = @config.directive_value(directive)
140106

141107
if source_list != OPT_OUT && source_list && source_list.any?
142108
normalized_source_list = minify_source_list(directive, source_list)
@@ -219,13 +185,13 @@ def append_nonce(source_list, nonce)
219185
source_list
220186
end
221187

222-
# Private: return the list of directives that are supported by the user agent,
188+
# Private: return the list of directives,
223189
# starting with default-src and ending with report-uri.
224190
def directives
225191
[
226192
DEFAULT_SRC,
227-
BODY_DIRECTIVES.select { |key| supported_directives.include?(key) },
228-
REPORT_URI
193+
BODY_DIRECTIVES,
194+
REPORT_URI,
229195
].flatten
230196
end
231197

@@ -234,25 +200,6 @@ def strip_source_schemes(source_list)
234200
source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") }
235201
end
236202

237-
# Private: determine which directives are supported for the given user agent.
238-
#
239-
# Add UA-sniffing special casing here.
240-
#
241-
# Returns an array of symbols representing the directives.
242-
def supported_directives
243-
@supported_directives ||= if VARIATIONS[@parsed_ua.browser]
244-
if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46)
245-
VARIATIONS["FirefoxTransitional"]
246-
elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10)
247-
VARIATIONS["SafariTransitional"]
248-
else
249-
VARIATIONS[@parsed_ua.browser]
250-
end
251-
else
252-
VARIATIONS[OTHER]
253-
end
254-
end
255-
256203
def symbol_to_hyphen_case(sym)
257204
sym.to_s.tr("_", "-")
258205
end

lib/secure_headers/headers/policy_management.rb

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -81,58 +81,12 @@ def self.included(base)
8181
UPGRADE_INSECURE_REQUESTS
8282
].flatten.freeze
8383

84-
EDGE_DIRECTIVES = DIRECTIVES_1_0
85-
SAFARI_DIRECTIVES = DIRECTIVES_1_0
86-
SAFARI_10_DIRECTIVES = DIRECTIVES_2_0
87-
88-
FIREFOX_UNSUPPORTED_DIRECTIVES = [
89-
BLOCK_ALL_MIXED_CONTENT,
90-
CHILD_SRC,
91-
WORKER_SRC,
92-
PLUGIN_TYPES
93-
].freeze
94-
95-
FIREFOX_46_DEPRECATED_DIRECTIVES = [
96-
FRAME_SRC
97-
].freeze
98-
99-
FIREFOX_46_UNSUPPORTED_DIRECTIVES = [
100-
BLOCK_ALL_MIXED_CONTENT,
101-
WORKER_SRC,
102-
PLUGIN_TYPES
103-
].freeze
104-
105-
FIREFOX_DIRECTIVES = (
106-
DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES
107-
).freeze
108-
109-
FIREFOX_46_DIRECTIVES = (
110-
DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES
111-
).freeze
112-
113-
CHROME_DIRECTIVES = (
114-
DIRECTIVES_3_0
115-
).freeze
116-
11784
ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort
11885

11986
# Think of default-src and report-uri as the beginning and end respectively,
12087
# everything else is in between.
12188
BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI]
12289

123-
VARIATIONS = {
124-
"Chrome" => CHROME_DIRECTIVES,
125-
"Opera" => CHROME_DIRECTIVES,
126-
"Firefox" => FIREFOX_DIRECTIVES,
127-
"FirefoxTransitional" => FIREFOX_46_DIRECTIVES,
128-
"Safari" => SAFARI_DIRECTIVES,
129-
"SafariTransitional" => SAFARI_10_DIRECTIVES,
130-
"Edge" => EDGE_DIRECTIVES,
131-
"Other" => CHROME_DIRECTIVES
132-
}.freeze
133-
134-
OTHER = "Other".freeze
135-
13690
DIRECTIVE_VALUE_TYPES = {
13791
BASE_URI => :source_list,
13892
BLOCK_ALL_MIXED_CONTENT => :boolean,
@@ -199,9 +153,9 @@ module ClassMethods
199153
#
200154
# Returns a default policy if no configuration is provided, or a
201155
# header name and value based on the config.
202-
def make_header(config, user_agent = nil)
156+
def make_header(config)
203157
return if config.nil? || config == OPT_OUT
204-
header = new(config, user_agent)
158+
header = new(config)
205159
[header.name, header.value]
206160
end
207161

secure_headers.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,4 @@ Gem::Specification.new do |gem|
1616
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
1717
gem.require_paths = ["lib"]
1818
gem.add_development_dependency "rake"
19-
gem.add_dependency "useragent", ">= 0.15.0"
2019
end

spec/lib/secure_headers/headers/content_security_policy_spec.rb

Lines changed: 1 addition & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -116,73 +116,10 @@ module SecureHeaders
116116
ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value
117117
end
118118

119-
it "raises an error when child-src and frame-src are supplied but are not equal" do
120-
expect {
121-
ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value
122-
}.to raise_error(ArgumentError)
123-
end
124-
125119
it "supports strict-dynamic" do
126-
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome])
120+
csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456})
127121
expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456' 'unsafe-inline'")
128122
end
129-
130-
context "browser sniffing" do
131-
let (:complex_opts) do
132-
(ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash|
133-
hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"]
134-
end.merge({
135-
block_all_mixed_content: true,
136-
upgrade_insecure_requests: true,
137-
script_src: %w(script-src.com),
138-
script_nonce: 123456,
139-
sandbox: %w(allow-forms),
140-
plugin_types: %w(application/pdf)
141-
})
142-
end
143-
144-
it "does not filter any directives for Chrome" do
145-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome])
146-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
147-
end
148-
149-
it "does not filter any directives for Opera" do
150-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera])
151-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; block-all-mixed-content; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; worker-src worker-src.com; report-uri report-uri.com")
152-
end
153-
154-
it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do
155-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox])
156-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
157-
end
158-
159-
it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do
160-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46])
161-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
162-
end
163-
164-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for Edge" do
165-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge])
166-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
167-
end
168-
169-
it "child-src value is copied to frame-src, adds 'unsafe-inline', filters base-uri, blocked-all-mixed-content, upgrade-insecure-requests, child-src, form-action, frame-ancestors, hash sources, and plugin-types for safari" do
170-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6])
171-
expect(policy.value).to eq("default-src default-src.com; connect-src connect-src.com; font-src font-src.com; frame-src child-src.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
172-
end
173-
174-
it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, and hash sources for safari 10 and higher" do
175-
policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10])
176-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; child-src child-src.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; img-src img-src.com; media-src media-src.com; object-src object-src.com; plugin-types application/pdf; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com")
177-
end
178-
179-
it "falls back to standard Firefox defaults when the useragent version is not present" do
180-
ua = USER_AGENTS[:firefox].dup
181-
allow(ua).to receive(:version).and_return(nil)
182-
policy = ContentSecurityPolicy.new(complex_opts, ua)
183-
expect(policy.value).to eq("default-src default-src.com; base-uri base-uri.com; connect-src connect-src.com; font-src font-src.com; form-action form-action.com; frame-ancestors frame-ancestors.com; frame-src child-src.com; img-src img-src.com; manifest-src manifest-src.com; media-src media-src.com; object-src object-src.com; sandbox allow-forms; script-src script-src.com 'nonce-123456' 'unsafe-inline'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com")
184-
end
185-
end
186123
end
187124
end
188125
end

spec/lib/secure_headers/headers/policy_management_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ module SecureHeaders
190190
report_uri = "https://report-uri.io/asdf"
191191
default_policy = Configuration.dup
192192
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_uri: [report_uri])
193-
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
193+
csp = ContentSecurityPolicy.new(combined_config)
194194
expect(csp.value).to include("report-uri #{report_uri}")
195195
end
196196

@@ -223,7 +223,7 @@ module SecureHeaders
223223
end
224224
default_policy = Configuration.dup
225225
combined_config = ContentSecurityPolicy.combine_policies(default_policy.csp.to_h, report_only: true)
226-
csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox])
226+
csp = ContentSecurityPolicy.new(combined_config)
227227
expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME)
228228
end
229229

spec/lib/secure_headers_spec.rb

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,6 @@ module SecureHeaders
127127
expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN)
128128
end
129129

130-
it "produces a UA-specific CSP when overriding (and busting the cache)" do
131-
Configuration.default do |config|
132-
config.csp = {
133-
default_src: %w('self'),
134-
script_src: %w('self'),
135-
child_src: %w('self')
136-
}
137-
end
138-
firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox]))
139-
140-
# append an unsupported directive
141-
SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(application/pdf)})
142-
# append a supported directive
143-
SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')})
144-
145-
hash = SecureHeaders.header_hash_for(firefox_request)
146-
147-
# child-src is translated to frame-src
148-
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'")
149-
end
150-
151130
it "produces a hash of headers with default config" do
152131
Configuration.default
153132
hash = SecureHeaders.header_hash_for(request)
@@ -197,22 +176,6 @@ module SecureHeaders
197176
expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com")
198177
end
199178

200-
it "child-src and frame-src must match" do
201-
Configuration.default do |config|
202-
config.csp = {
203-
default_src: %w('self'),
204-
frame_src: %w(frame_src.com),
205-
script_src: %w('self')
206-
}
207-
end
208-
209-
SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com))
210-
211-
expect {
212-
SecureHeaders.header_hash_for(chrome_request)
213-
}.to raise_error(ArgumentError)
214-
end
215-
216179
it "supports named appends" do
217180
Configuration.default do |config|
218181
config.csp = {

0 commit comments

Comments
 (0)