Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,29 @@ end

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

## Disabling secure_headers
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this is important enough to be in the README or would it make sense to document it elsewhere?


If you want to disable `secure_headers` entirely (e.g., for specific environments or deployment scenarios), you can use `Configuration.disable!`:

```ruby
if ENV["ENABLE_STRICT_HEADERS"]
SecureHeaders::Configuration.default do |config|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is a configuration setting, this can only be done at server startup right? I think that's implied but would it be valuable to make that clearer in case someone gets the idea that they could disable this during runtime? I could go either way on whether or not that's overkill

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I think that's worth documenting.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've also made it so that they're mutually exclusive preventing one from trying to configure and then disable.

# your configuration here
end
else
SecureHeaders::Configuration.disable!
end
```

**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`.

When disabled, no security headers will be set by the gem. This is useful when:
- You're gradually rolling out secure_headers across different customers or deployments
- You need to migrate existing custom headers to secure_headers
- You want environment-specific control over security headers

Note: When `disable!` is used, you don't need to configure a default configuration. The gem will not raise a `NotYetConfiguredError`.

## Acknowledgements

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.
Expand Down
54 changes: 49 additions & 5 deletions lib/secure_headers/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,53 @@ class AlreadyConfiguredError < StandardError; end
class NotYetConfiguredError < StandardError; end
class IllegalPolicyModificationError < StandardError; end
class << self
# Public: Disable secure_headers entirely. When disabled, no headers will be set.
#
# Note: This must be called before Configuration.default. Calling it after
# Configuration.default has been set will raise an AlreadyConfiguredError.
#
# Returns nothing
# Raises AlreadyConfiguredError if Configuration.default has already been called
def disable!
if defined?(@default_config)
raise AlreadyConfiguredError, "Configuration already set, cannot disable"
end

@disabled = true
@noop_config = create_noop_config.freeze

# Ensure the built-in NOOP override is available even if `default` has never been called
@overrides ||= {}
unless @overrides.key?(NOOP_OVERRIDE)
@overrides[NOOP_OVERRIDE] = method(:create_noop_config_block)
end
end

# Public: Check if secure_headers is disabled
#
# Returns boolean
def disabled?
defined?(@disabled) && @disabled
end

# Public: Set the global default configuration.
#
# Optionally supply a block to override the defaults set by this library.
#
# Returns the newly created config.
# Raises AlreadyConfiguredError if Configuration.disable! has already been called
def default(&block)
if disabled?
raise AlreadyConfiguredError, "Configuration has been disabled, cannot set default"
end

if defined?(@default_config)
raise AlreadyConfiguredError, "Policy already configured"
end

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

new_config = new(&block).freeze
new_config.validate_config!
Expand Down Expand Up @@ -101,6 +131,7 @@ def deep_copy(config)
# of ensuring that the default config is never mutated and is dup(ed)
# before it is used in a request.
def default_config
return @noop_config if disabled?
unless defined?(@default_config)
raise NotYetConfiguredError, "Default policy not yet configured"
end
Expand All @@ -116,6 +147,19 @@ def deep_copy_if_hash(value)
value
end
end

# Private: Creates a NOOP configuration that opts out of all headers
def create_noop_config
new(&method(:create_noop_config_block))
end

# Private: Block for creating NOOP configuration
# Used by both create_noop_config and the NOOP_OVERRIDE mechanism
def create_noop_config_block(config)
CONFIG_ATTRIBUTES.each do |attr|
config.instance_variable_set("@#{attr}", OPT_OUT)
end
end
end

CONFIG_ATTRIBUTES_TO_HEADER_CLASSES = {
Expand Down
89 changes: 89 additions & 0 deletions spec/lib/secure_headers/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,94 @@ module SecureHeaders
config = Configuration.dup
expect(config.cookies).to eq({httponly: true, secure: true, samesite: {lax: false}})
end

describe ".disable!" do
it "disables secure_headers completely" do
Configuration.disable!
expect(Configuration.disabled?).to be true
end

it "returns a noop config when disabled" do
Configuration.disable!
config = Configuration.send(:default_config)
Configuration::CONFIG_ATTRIBUTES.each do |attr|
expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT)
end
end

it "does not raise NotYetConfiguredError when disabled without default config" do
Configuration.disable!
expect { Configuration.send(:default_config) }.not_to raise_error
end

it "registers the NOOP_OVERRIDE when disabled without calling default" do
Configuration.disable!
expect(Configuration.overrides(Configuration::NOOP_OVERRIDE)).to_not be_nil
end

it "raises AlreadyConfiguredError when called after default" do
Configuration.default do |config|
config.csp = { default_src: %w('self'), script_src: %w('self') }
end

expect {
Configuration.disable!
}.to raise_error(Configuration::AlreadyConfiguredError, "Configuration already set, cannot disable")
end

it "raises AlreadyConfiguredError when default is called after disable!" do
Configuration.disable!

expect {
Configuration.default do |config|
config.csp = { default_src: %w('self'), script_src: %w('self') }
end
}.to raise_error(Configuration::AlreadyConfiguredError, "Configuration has been disabled, cannot set default")
end

it "allows default to be called after disable! and reset_config" do
Configuration.disable!
reset_config

expect {
Configuration.default do |config|
config.csp = { default_src: %w('self'), script_src: %w('self') }
end
}.not_to raise_error

# After reset_config, disabled? returns nil (not false) because @disabled is removed
expect(Configuration.disabled?).to be_falsy
expect(Configuration.instance_variable_defined?(:@default_config)).to be true
end

it "works correctly with dup when library is disabled" do
Configuration.disable!
config = Configuration.dup

Configuration::CONFIG_ATTRIBUTES.each do |attr|
expect(config.instance_variable_get("@#{attr}")).to eq(OPT_OUT)
end
end

it "does not interfere with override mechanism" do
Configuration.disable!

# Should be able to use opt_out_of_all_protection without error
request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on")
expect {
SecureHeaders.opt_out_of_all_protection(request)
}.not_to raise_error
end

it "interacts correctly with named overrides when disabled" do
Configuration.disable!

Configuration.override(:test_override) do |config|
config.x_frame_options = "DENY"
end

expect(Configuration.overrides(:test_override)).to_not be_nil
end
end
end
end
28 changes: 28 additions & 0 deletions spec/lib/secure_headers/middleware_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,33 @@ module SecureHeaders
expect(env["Set-Cookie"]).to eq("foo=bar; secure")
end
end

context "when disabled" do
before(:each) do
reset_config
Configuration.disable!
end

it "does not set any headers" do
_, env = middleware.call(Rack::MockRequest.env_for("https://localhost", {}))

# Verify no security headers are set by checking all configured header classes
Configuration::HEADERABLE_ATTRIBUTES.each do |attr|
klass = Configuration::CONFIG_ATTRIBUTES_TO_HEADER_CLASSES[attr]
# Handle CSP specially since it has multiple classes
if attr == :csp
expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil
expect(env[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil
elsif klass.const_defined?(:HEADER_NAME)
expect(env[klass::HEADER_NAME]).to be_nil
end
end
end

it "does not flag cookies" do
_, env = cookie_middleware.call(Rack::MockRequest.env_for("https://localhost", {}))
expect(env["Set-Cookie"]).to eq("foo=bar")
end
end
end
end
6 changes: 6 additions & 0 deletions spec/lib/secure_headers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ module SecureHeaders
expect(hash.count).to eq(0)
end

it "allows you to disable secure_headers entirely via Configuration.disable!" do
Configuration.disable!
hash = SecureHeaders.header_hash_for(request)
expect(hash.count).to eq(0)
end

it "allows you to override x-frame-options settings" do
Configuration.default
SecureHeaders.override_x_frame_options(request, XFrameOptions::DENY)
Expand Down
6 changes: 6 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ def clear_overrides
def clear_appends
remove_instance_variable(:@appends) if defined?(@appends)
end

def clear_disabled
remove_instance_variable(:@disabled) if defined?(@disabled)
remove_instance_variable(:@noop_config) if defined?(@noop_config)
end
end
end
end
Expand All @@ -61,4 +66,5 @@ def reset_config
SecureHeaders::Configuration.clear_default_config
SecureHeaders::Configuration.clear_overrides
SecureHeaders::Configuration.clear_appends
SecureHeaders::Configuration.clear_disabled
end