secure_headers 3.0.3 → 4.0.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +41 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  4. data/.gitignore +0 -9
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +3 -0
  7. data/.ruby-version +1 -1
  8. data/.travis.yml +17 -5
  9. data/CHANGELOG.md +287 -0
  10. data/CODE_OF_CONDUCT.md +46 -0
  11. data/CONTRIBUTING.md +41 -0
  12. data/Gemfile +13 -5
  13. data/Guardfile +1 -0
  14. data/LICENSE +4 -199
  15. data/README.md +85 -265
  16. data/Rakefile +22 -18
  17. data/docs/HPKP.md +17 -0
  18. data/docs/cookies.md +64 -0
  19. data/docs/hashes.md +64 -0
  20. data/docs/named_overrides_and_appends.md +107 -0
  21. data/docs/per_action_configuration.md +133 -0
  22. data/docs/sinatra.md +25 -0
  23. data/lib/secure_headers/configuration.rb +188 -59
  24. data/lib/secure_headers/hash_helper.rb +11 -0
  25. data/lib/secure_headers/headers/clear_site_data.rb +55 -0
  26. data/lib/secure_headers/headers/content_security_policy.rb +136 -325
  27. data/lib/secure_headers/headers/content_security_policy_config.rb +162 -0
  28. data/lib/secure_headers/headers/cookie.rb +144 -0
  29. data/lib/secure_headers/headers/expect_certificate_transparency.rb +70 -0
  30. data/lib/secure_headers/headers/policy_management.rb +410 -0
  31. data/lib/secure_headers/headers/public_key_pins.rb +5 -4
  32. data/lib/secure_headers/headers/referrer_policy.rb +37 -0
  33. data/lib/secure_headers/headers/strict_transport_security.rb +2 -1
  34. data/lib/secure_headers/headers/x_content_type_options.rb +3 -2
  35. data/lib/secure_headers/headers/x_download_options.rb +3 -2
  36. data/lib/secure_headers/headers/x_frame_options.rb +2 -1
  37. data/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb +3 -2
  38. data/lib/secure_headers/headers/x_xss_protection.rb +2 -1
  39. data/lib/secure_headers/middleware.rb +44 -0
  40. data/lib/secure_headers/railtie.rb +11 -6
  41. data/lib/secure_headers/utils/cookies_config.rb +95 -0
  42. data/lib/secure_headers/view_helper.rb +76 -5
  43. data/lib/secure_headers.rb +159 -99
  44. data/lib/tasks/tasks.rake +83 -0
  45. data/secure_headers.gemspec +13 -3
  46. data/spec/lib/secure_headers/configuration_spec.rb +24 -9
  47. data/spec/lib/secure_headers/headers/clear_site_data_spec.rb +87 -0
  48. data/spec/lib/secure_headers/headers/content_security_policy_spec.rb +88 -189
  49. data/spec/lib/secure_headers/headers/cookie_spec.rb +182 -0
  50. data/spec/lib/secure_headers/headers/expect_certificate_spec.rb +42 -0
  51. data/spec/lib/secure_headers/headers/policy_management_spec.rb +228 -0
  52. data/spec/lib/secure_headers/headers/public_key_pins_spec.rb +7 -6
  53. data/spec/lib/secure_headers/headers/referrer_policy_spec.rb +73 -0
  54. data/spec/lib/secure_headers/headers/strict_transport_security_spec.rb +6 -5
  55. data/spec/lib/secure_headers/headers/x_content_type_options_spec.rb +2 -1
  56. data/spec/lib/secure_headers/headers/x_download_options_spec.rb +3 -2
  57. data/spec/lib/secure_headers/headers/x_frame_options_spec.rb +2 -1
  58. data/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb +4 -3
  59. data/spec/lib/secure_headers/headers/x_xss_protection_spec.rb +4 -3
  60. data/spec/lib/secure_headers/middleware_spec.rb +96 -6
  61. data/spec/lib/secure_headers/view_helpers_spec.rb +138 -0
  62. data/spec/lib/secure_headers_spec.rb +407 -58
  63. data/spec/spec_helper.rb +19 -12
  64. data/upgrading-to-3-0.md +13 -9
  65. data/upgrading-to-4-0.md +55 -0
  66. metadata +45 -9
  67. data/lib/secure_headers/padrino.rb +0 -13
  68. data/travis.sh +0 -10
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ module DynamicConfig
4
+ def self.included(base)
5
+ base.send(:attr_writer, :modified)
6
+ base.send(:attr_reader, *base.attrs)
7
+ base.attrs.each do |attr|
8
+ base.send(:define_method, "#{attr}=") do |value|
9
+ if self.class.attrs.include?(attr)
10
+ write_attribute(attr, value)
11
+ else
12
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}"
13
+ end
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(hash)
19
+ @base_uri = nil
20
+ @block_all_mixed_content = nil
21
+ @child_src = nil
22
+ @connect_src = nil
23
+ @default_src = nil
24
+ @font_src = nil
25
+ @form_action = nil
26
+ @frame_ancestors = nil
27
+ @frame_src = nil
28
+ @img_src = nil
29
+ @manifest_src = nil
30
+ @media_src = nil
31
+ @object_src = nil
32
+ @plugin_types = nil
33
+ @preserve_schemes = nil
34
+ @report_only = nil
35
+ @report_uri = nil
36
+ @sandbox = nil
37
+ @script_nonce = nil
38
+ @script_src = nil
39
+ @style_nonce = nil
40
+ @style_src = nil
41
+ @upgrade_insecure_requests = nil
42
+
43
+ from_hash(hash)
44
+ @modified = false
45
+ end
46
+
47
+ def update_directive(directive, value)
48
+ self.send("#{directive}=", value)
49
+ end
50
+
51
+ def directive_value(directive)
52
+ if self.class.attrs.include?(directive)
53
+ self.send(directive)
54
+ end
55
+ end
56
+
57
+ def modified?
58
+ @modified
59
+ end
60
+
61
+ def merge(new_hash)
62
+ ContentSecurityPolicy.combine_policies(self.to_h, new_hash)
63
+ end
64
+
65
+ def merge!(new_hash)
66
+ from_hash(new_hash)
67
+ end
68
+
69
+ def append(new_hash)
70
+ from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash))
71
+ end
72
+
73
+ def to_h
74
+ self.class.attrs.each_with_object({}) do |key, hash|
75
+ value = self.send(key)
76
+ hash[key] = value unless value.nil?
77
+ end
78
+ end
79
+
80
+ def dup
81
+ self.class.new(self.to_h)
82
+ end
83
+
84
+ def opt_out?
85
+ false
86
+ end
87
+
88
+ def ==(o)
89
+ self.class == o.class && self.to_h == o.to_h
90
+ end
91
+
92
+ alias_method :[], :directive_value
93
+ alias_method :[]=, :update_directive
94
+
95
+ private
96
+ def from_hash(hash)
97
+ hash.each_pair do |k, v|
98
+ next if v.nil?
99
+
100
+ if self.class.attrs.include?(k)
101
+ write_attribute(k, v)
102
+ else
103
+ raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}"
104
+ end
105
+ end
106
+ end
107
+
108
+ def write_attribute(attr, value)
109
+ value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list
110
+ attr_variable = "@#{attr}"
111
+ prev_value = self.instance_variable_get(attr_variable)
112
+ self.instance_variable_set(attr_variable, value)
113
+ if prev_value != value
114
+ @modified = true
115
+ end
116
+ end
117
+ end
118
+
119
+ class ContentSecurityPolicyConfigError < StandardError; end
120
+ class ContentSecurityPolicyConfig
121
+ CONFIG_KEY = :csp
122
+ HEADER_NAME = "Content-Security-Policy".freeze
123
+
124
+ ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES
125
+ def self.attrs
126
+ ATTRS
127
+ end
128
+
129
+ include DynamicConfig
130
+
131
+ # based on what was suggested in https://github.com/rails/rails/pull/24961/files
132
+ DEFAULT = {
133
+ default_src: %w('self' https:),
134
+ font_src: %w('self' https: data:),
135
+ img_src: %w('self' https: data:),
136
+ object_src: %w('none'),
137
+ script_src: %w(https:),
138
+ style_src: %w('self' https: 'unsafe-inline')
139
+ }
140
+
141
+ def report_only?
142
+ false
143
+ end
144
+
145
+ def make_report_only
146
+ ContentSecurityPolicyReportOnlyConfig.new(self.to_h)
147
+ end
148
+ end
149
+
150
+ class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig
151
+ CONFIG_KEY = :csp_report_only
152
+ HEADER_NAME = "Content-Security-Policy-Report-Only".freeze
153
+
154
+ def report_only?
155
+ true
156
+ end
157
+
158
+ def make_report_only
159
+ self
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ require "cgi"
3
+ require "secure_headers/utils/cookies_config"
4
+
5
+
6
+ module SecureHeaders
7
+ class CookiesConfigError < StandardError; end
8
+ class Cookie
9
+
10
+ class << self
11
+ def validate_config!(config)
12
+ CookiesConfig.new(config).validate!
13
+ end
14
+ end
15
+
16
+ attr_reader :raw_cookie, :config
17
+
18
+ COOKIE_DEFAULTS = {
19
+ httponly: true,
20
+ secure: true,
21
+ samesite: { lax: true },
22
+ }.freeze
23
+
24
+ def initialize(cookie, config)
25
+ @raw_cookie = cookie
26
+ unless config == OPT_OUT
27
+ config ||= {}
28
+ config = COOKIE_DEFAULTS.merge(config)
29
+ end
30
+ @config = config
31
+ @attributes = {
32
+ httponly: nil,
33
+ samesite: nil,
34
+ secure: nil,
35
+ }
36
+
37
+ parse(cookie)
38
+ end
39
+
40
+ def to_s
41
+ @raw_cookie.dup.tap do |c|
42
+ c << "; secure" if secure?
43
+ c << "; HttpOnly" if httponly?
44
+ c << "; #{samesite_cookie}" if samesite?
45
+ end
46
+ end
47
+
48
+ def secure?
49
+ flag_cookie?(:secure) && !already_flagged?(:secure)
50
+ end
51
+
52
+ def httponly?
53
+ flag_cookie?(:httponly) && !already_flagged?(:httponly)
54
+ end
55
+
56
+ def samesite?
57
+ flag_samesite? && !already_flagged?(:samesite)
58
+ end
59
+
60
+ private
61
+
62
+ def parsed_cookie
63
+ @parsed_cookie ||= CGI::Cookie.parse(raw_cookie)
64
+ end
65
+
66
+ def already_flagged?(attribute)
67
+ @attributes[attribute]
68
+ end
69
+
70
+ def flag_cookie?(attribute)
71
+ return false if config == OPT_OUT
72
+ case config[attribute]
73
+ when TrueClass
74
+ true
75
+ when Hash
76
+ conditionally_flag?(config[attribute])
77
+ else
78
+ false
79
+ end
80
+ end
81
+
82
+ def conditionally_flag?(configuration)
83
+ if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?)
84
+ true
85
+ elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?)
86
+ true
87
+ else
88
+ false
89
+ end
90
+ end
91
+
92
+ def samesite_cookie
93
+ if flag_samesite_lax?
94
+ "SameSite=Lax"
95
+ elsif flag_samesite_strict?
96
+ "SameSite=Strict"
97
+ end
98
+ end
99
+
100
+ def flag_samesite?
101
+ return false if config == OPT_OUT || config[:samesite] == OPT_OUT
102
+ flag_samesite_lax? || flag_samesite_strict?
103
+ end
104
+
105
+ def flag_samesite_lax?
106
+ flag_samesite_enforcement?(:lax)
107
+ end
108
+
109
+ def flag_samesite_strict?
110
+ flag_samesite_enforcement?(:strict)
111
+ end
112
+
113
+ def flag_samesite_enforcement?(mode)
114
+ return unless config[:samesite]
115
+
116
+ if config[:samesite].is_a?(TrueClass) && mode == :lax
117
+ return true
118
+ end
119
+
120
+ case config[:samesite][mode]
121
+ when Hash
122
+ conditionally_flag?(config[:samesite][mode])
123
+ when TrueClass
124
+ true
125
+ else
126
+ false
127
+ end
128
+ end
129
+
130
+ def parse(cookie)
131
+ return unless cookie
132
+
133
+ cookie.split(/[;,]\s?/).each do |pairs|
134
+ name, values = pairs.split("=", 2)
135
+ name = CGI.unescape(name)
136
+
137
+ attribute = name.downcase.to_sym
138
+ if @attributes.has_key?(attribute)
139
+ @attributes[attribute] = values || true
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module SecureHeaders
3
+ class ExpectCertificateTransparencyConfigError < StandardError; end
4
+
5
+ class ExpectCertificateTransparency
6
+ HEADER_NAME = "Expect-CT".freeze
7
+ CONFIG_KEY = :expect_certificate_transparency
8
+ INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
9
+ INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
10
+ REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
11
+ INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze
12
+
13
+ class << self
14
+ # Public: Generate a Expect-CT header.
15
+ #
16
+ # Returns nil if not configured, returns header name and value if
17
+ # configured.
18
+ def make_header(config)
19
+ return if config.nil?
20
+
21
+ header = new(config)
22
+ [HEADER_NAME, header.value]
23
+ end
24
+
25
+ def validate_config!(config)
26
+ return if config.nil? || config == OPT_OUT
27
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash
28
+
29
+ unless [true, false, nil].include?(config[:enforce])
30
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
31
+ end
32
+
33
+ if !config[:max_age]
34
+ raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
35
+ elsif config[:max_age].to_s !~ /\A\d+\z/
36
+ raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(config)
42
+ @enforced = config.fetch(:enforce, nil)
43
+ @max_age = config.fetch(:max_age, nil)
44
+ @report_uri = config.fetch(:report_uri, nil)
45
+ end
46
+
47
+ def value
48
+ [
49
+ enforced_directive,
50
+ max_age_directive,
51
+ report_uri_directive
52
+ ].compact.join("; ").strip
53
+ end
54
+
55
+ def enforced_directive
56
+ # Unfortunately `if @enforced` isn't enough here in case someone
57
+ # passes in a random string so let's be specific with it to prevent
58
+ # accidental enforcement.
59
+ "enforce" if @enforced == true
60
+ end
61
+
62
+ def max_age_directive
63
+ "max-age=#{@max_age}" if @max_age
64
+ end
65
+
66
+ def report_uri_directive
67
+ "report-uri=\"#{@report_uri}\"" if @report_uri
68
+ end
69
+ end
70
+ end