Skip to content

Commit 6ac6e72

Browse files
Copilotfletchto99
authored andcommitted
Add Configuration.disable! to completely disable secure_headers
Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Add vendor/bundle to .gitignore and remove from repository Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Document Configuration.disable! in README Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Refactor NOOP config creation to eliminate code duplication Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Address code review feedback: improve comments and test maintainability Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Address PR review feedback: Fix edge cases and improve consistency - Register NOOP_OVERRIDE in disable! to avoid ArgumentError - Clear @default_config when disable! is called after default - Move clear_disabled inside class << self for consistency - Add comprehensive edge case tests (calling disable! after default, default after disable!, interaction with overrides/dup) - Document behavior in method comments Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Raise AlreadyConfiguredError when disable! and default conflict - disable! now raises AlreadyConfiguredError if default has been called - default now raises AlreadyConfiguredError if disable! has been called - Updated tests to verify error conditions - Updated documentation to reflect mutual exclusion Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Revert .gitignore changes Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Apply suggestions from code review Document that disable!/default must be set at startup Clarify in README that Configuration.disable! and Configuration.default must be called during application startup and cannot be changed at runtime. Co-authored-by: fletchto99 <718681+fletchto99@users.noreply.github.com> Update spec/lib/secure_headers/middleware_spec.rb
1 parent fcf1723 commit 6ac6e72

File tree

6 files changed

+201
-5
lines changed

6 files changed

+201
-5
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ end
125125

126126
However, I would consider these headers anyways depending on your load and bandwidth requirements.
127127

128+
## Disabling secure_headers
129+
130+
If you want to disable `secure_headers` entirely (e.g., for specific environments or deployment scenarios), you can use `Configuration.disable!`:
131+
132+
```ruby
133+
if ENV["ENABLE_STRICT_HEADERS"]
134+
SecureHeaders::Configuration.default do |config|
135+
# your configuration here
136+
end
137+
else
138+
SecureHeaders::Configuration.disable!
139+
end
140+
```
141+
142+
**Important**: This configuration must be set during application startup (e.g., in an initializer). Once you call either `Configuration.default` or `Configuration.disable!`, the choice cannot be changed at runtime. Attempting to call `disable!` after `default` (or vice versa) will raise an `AlreadyConfiguredError`.
143+
144+
When disabled, no security headers will be set by the gem. This is useful when:
145+
- You're gradually rolling out secure_headers across different customers or deployments
146+
- You need to migrate existing custom headers to secure_headers
147+
- You want environment-specific control over security headers
148+
149+
Note: When `disable!` is used, you don't need to configure a default configuration. The gem will not raise a `NotYetConfiguredError`.
150+
128151
## Acknowledgements
129152

130153
This project originated within the Security team at Twitter. An archived fork from the point of transition is here: https://github.com/twitter-archive/secure_headers.

lib/secure_headers/configuration.rb

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,23 +9,53 @@ class AlreadyConfiguredError < StandardError; end
99
class NotYetConfiguredError < StandardError; end
1010
class IllegalPolicyModificationError < StandardError; end
1111
class << self
12+
# Public: Disable secure_headers entirely. When disabled, no headers will be set.
13+
#
14+
# Note: This must be called before Configuration.default. Calling it after
15+
# Configuration.default has been set will raise an AlreadyConfiguredError.
16+
#
17+
# Returns nothing
18+
# Raises AlreadyConfiguredError if Configuration.default has already been called
19+
def disable!
20+
if defined?(@default_config)
21+
raise AlreadyConfiguredError, "Configuration already set, cannot disable"
22+
end
23+
24+
@disabled = true
25+
@noop_config = create_noop_config.freeze
26+
27+
# Ensure the built-in NOOP override is available even if `default` has never been called
28+
@overrides ||= {}
29+
unless @overrides.key?(NOOP_OVERRIDE)
30+
@overrides[NOOP_OVERRIDE] = method(:create_noop_config_block)
31+
end
32+
end
33+
34+
# Public: Check if secure_headers is disabled
35+
#
36+
# Returns boolean
37+
def disabled?
38+
defined?(@disabled) && @disabled
39+
end
40+
1241
# Public: Set the global default configuration.
1342
#
1443
# Optionally supply a block to override the defaults set by this library.
1544
#
1645
# Returns the newly created config.
46+
# Raises AlreadyConfiguredError if Configuration.disable! has already been called
1747
def default(&block)
48+
if disabled?
49+
raise AlreadyConfiguredError, "Configuration has been disabled, cannot set default"
50+
end
51+
1852
if defined?(@default_config)
1953
raise AlreadyConfiguredError, "Policy already configured"
2054
end
2155

2256
# Define a built-in override that clears all configuration options and
2357
# results in no security headers being set.
24-
override(NOOP_OVERRIDE) do |config|
25-
CONFIG_ATTRIBUTES.each do |attr|
26-
config.instance_variable_set("@#{attr}", OPT_OUT)
27-
end
28-
end
58+
override(NOOP_OVERRIDE, &method(:create_noop_config_block))
2959

3060
new_config = new(&block).freeze
3161
new_config.validate_config!
@@ -101,6 +131,7 @@ def deep_copy(config)
101131
# of ensuring that the default config is never mutated and is dup(ed)
102132
# before it is used in a request.
103133
def default_config
134+
return @noop_config if disabled?
104135
unless defined?(@default_config)
105136
raise NotYetConfiguredError, "Default policy not yet configured"
106137
end
@@ -116,6 +147,19 @@ def deep_copy_if_hash(value)
116147
value
117148
end
118149
end
150+
151+
# Private: Creates a NOOP configuration that opts out of all headers
152+
def create_noop_config
153+
new(&method(:create_noop_config_block))
154+
end
155+
156+
# Private: Block for creating NOOP configuration
157+
# Used by both create_noop_config and the NOOP_OVERRIDE mechanism
158+
def create_noop_config_block(config)
159+
CONFIG_ATTRIBUTES.each do |attr|
160+
config.instance_variable_set("@#{attr}", OPT_OUT)
161+
end
162+
end
119163
end
120164

121165
CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {

spec/lib/secure_headers/configuration_spec.rb

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,5 +119,94 @@ module SecureHeaders
119119
config = Configuration.dup
120120
expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
121121
end
122+
123+
describe ".disable!" do
124+
it "disables secure_headers completely" do
125+
Configuration.disable!
126+
expect(Configuration.disabled?).to be true
127+
end
128+
129+
it "returns a noop config when disabled" do
130+
Configuration.disable!
131+
config = Configuration.send(:default_config)
132+
Configuration::CONFIG_ATTRIBUTES.each do |attr|
133+
expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT)
134+
end
135+
end
136+
137+
it "does not raise NotYetConfiguredError when disabled without default config" do
138+
Configuration.disable!
139+
expect { Configuration.send(:default_config) }.not_to raise_error
140+
end
141+
142+
it "registers the NOOP_OVERRIDE when disabled without calling default" do
143+
Configuration.disable!
144+
expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
145+
end
146+
147+
it "raises AlreadyConfiguredError when called after default" do
148+
Configuration.default do |config|
149+
config.csp = { default_src: %w('self'), script_src: %w('self') }
150+
end
151+
152+
expect {
153+
Configuration.disable!
154+
}.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already set, cannot disable")
155+
end
156+
157+
it "raises AlreadyConfiguredError when default is called after disable!" do
158+
Configuration.disable!
159+
160+
expect {
161+
Configuration.default do |config|
162+
config.csp = { default_src: %w('self'), script_src: %w('self') }
163+
end
164+
}.to raise_error(Configuration::AlreadyConfiguredError, "Configuration has been disabled, cannot set default")
165+
end
166+
167+
it "allows default to be called after disable! and reset_config" do
168+
Configuration.disable!
169+
reset_config
170+
171+
expect {
172+
Configuration.default do |config|
173+
config.csp = { default_src: %w('self'), script_src: %w('self') }
174+
end
175+
}.not_to raise_error
176+
177+
# After reset_config, disabled? returns nil (not false) because @disabled is removed
178+
expect(Configuration.disabled?).to be_falsy
179+
expect(Configuration.instance_variable_defined?(:@default_config)).to be true
180+
end
181+
182+
it "works correctly with dup when library is disabled" do
183+
Configuration.disable!
184+
config = Configuration.dup
185+
186+
Configuration::CONFIG_ATTRIBUTES.each do |attr|
187+
expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT)
188+
end
189+
end
190+
191+
it "does not interfere with override mechanism" do
192+
Configuration.disable!
193+
194+
# Should be able to use opt_out_of_all_protection without error
195+
request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
196+
expect {
197+
SecureHeaders.opt_out_of_all_protection(request)
198+
}.not_to raise_error
199+
end
200+
201+
it "interacts correctly with named overrides when disabled" do
202+
Configuration.disable!
203+
204+
Configuration.override(:test_override) do |config|
205+
config.x_frame_options = "DENY"
206+
end
207+
208+
expect(Configuration.overrides(:test_override)).to_not be_nil
209+
end
210+
end
122211
end
123212
end

spec/lib/secure_headers/middleware_spec.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,5 +123,33 @@ module SecureHeaders
123123
expect(env["Set-Cookie"]).to eq("foo=bar; secure")
124124
end
125125
end
126+
127+
context "when disabled" do
128+
before(:each) do
129+
reset_config
130+
Configuration.disable!
131+
end
132+
133+
it "does not set any headers" do
134+
_, env = middleware.call(Rack::MockRequest.env_for("https://localhost", {}))
135+
136+
# Verify no security headers are set by checking all configured header classes
137+
Configuration::HEADERABLE_ATTRIBUTES.each do |attr|
138+
klass = Configuration::CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
139+
# Handle CSP specially since it has multiple classes
140+
if attr == :csp
141+
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
142+
expect(env[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
143+
elsif klass.const_defined?(:HEADER_NAME)
144+
expect(env[klass::HEADER_NAME]).to be_nil
145+
end
146+
end
147+
end
148+
149+
it "does not flag cookies" do
150+
_, env = cookie_middleware.call(Rack::MockRequest.env_for("https://localhost", {}))
151+
expect(env["Set-Cookie"]).to eq("foo=bar")
152+
end
153+
end
126154
end
127155
end

spec/lib/secure_headers_spec.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ module SecureHeaders
112112
expect(hash.count).to eq(0)
113113
end
114114

115+
it "allows you to disable secure_headers entirely via Configuration.disable!" do
116+
Configuration.disable!
117+
hash = SecureHeaders.header_hash_for(request)
118+
expect(hash.count).to eq(0)
119+
end
120+
115121
it "allows you to override x-frame-options settings" do
116122
Configuration.default
117123
SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)

spec/spec_helper.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ def clear_overrides
5353
def clear_appends
5454
remove_instance_variable(:@appends) if defined?(@appends)
5555
end
56+
57+
def clear_disabled
58+
remove_instance_variable(:@disabled) if defined?(@disabled)
59+
remove_instance_variable(:@noop_config) if defined?(@noop_config)
60+
end
5661
end
5762
end
5863
end
@@ -61,4 +66,5 @@ def reset_config
6166
SecureHeaders::Configuration.clear_default_config
6267
SecureHeaders::Configuration.clear_overrides
6368
SecureHeaders::Configuration.clear_appends
69+
SecureHeaders::Configuration.clear_disabled
6470
end

0 commit comments

Comments
 (0)