Skip to content

Commit

Permalink
Add support for Expect-CT HTTP header (#322)
Browse files Browse the repository at this point in the history
* Update README with example usage for `Expect-CT`

* Add some tests for the expected API

* Add classes and loading to main entrypoints

* Add `ExpectCt` class

Adds the `ExpectCt` class and it's associated validation of the header
syntax

* Use full name of specification instead

* Use consistent double quotes

* Update README example to use correct naming for Expect-CT

* Set defaults for Expect-CT via helper

* Add missing comma in README example

* Add frozen string literal
  • Loading branch information
jacobbednarz authored and oreoshake committed Aug 25, 2017
1 parent f06b926 commit 0038e25
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The gem will automatically apply several headers that are related to security.
- X-Permitted-Cross-Domain-Policies - [Restrict Adobe Flash Player's access to data](https://www.adobe.com/devnet/adobe-media-server/articles/cross-domain-xml-for-streaming.html)
- Referrer-Policy - [Referrer Policy draft](https://w3c.github.io/webappsec-referrer-policy/)
- Public Key Pinning - Pin certificate fingerprints in the browser to prevent man-in-the-middle attacks due to compromised Certificate Authorities. [Public Key Pinning Specification](https://tools.ietf.org/html/rfc7469)
- Expect-CT - Only use certificates that are present in the certificate transparency logs. [Expect-CT draft specification](https://datatracker.ietf.org/doc/draft-stark-expect-ct/).
- Clear-Site-Data - Clearing browser data for origin. [Clear-Site-Data specification](https://w3c.github.io/webappsec-clear-site-data/).

It can also mark all http cookies with the Secure, HttpOnly and SameSite attributes (when configured to do so).
Expand Down Expand Up @@ -76,6 +77,11 @@ SecureHeaders::Configuration.default do |config|
"storage",
"executionContexts"
]
config.expect_certificate_transparency = {
enforce: false,
max_age: 1.day.to_i,
report_uri: "https://report-uri.io/example-ct"
}
config.csp = {
# "meta" values. these will shaped the header, but the values are not included in the header.
# report_only: true, # default: false [DEPRECATED from 3.5.0: instead, configure csp_report_only]
Expand Down
2 changes: 2 additions & 0 deletions lib/secure_headers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require "secure_headers/headers/x_permitted_cross_domain_policies"
require "secure_headers/headers/referrer_policy"
require "secure_headers/headers/clear_site_data"
require "secure_headers/headers/expect_certificate_transparency"
require "secure_headers/middleware"
require "secure_headers/railtie"
require "secure_headers/view_helper"
Expand Down Expand Up @@ -51,6 +52,7 @@ def opt_out?
CSP = ContentSecurityPolicy

ALL_HEADER_CLASSES = [
ExpectCertificateTransparency,
ClearSiteData,
ContentSecurityPolicyConfig,
ContentSecurityPolicyReportOnlyConfig,
Expand Down
4 changes: 3 additions & 1 deletion lib/secure_headers/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def deep_copy_if_hash(value)

attr_writer :hsts, :x_frame_options, :x_content_type_options,
:x_xss_protection, :x_download_options, :x_permitted_cross_domain_policies,
:referrer_policy, :clear_site_data
:referrer_policy, :clear_site_data, :expect_certificate_transparency

attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host

Expand Down Expand Up @@ -168,6 +168,7 @@ def dup
copy.x_download_options = @x_download_options
copy.x_permitted_cross_domain_policies = @x_permitted_cross_domain_policies
copy.clear_site_data = @clear_site_data
copy.expect_certificate_transparency = @expect_certificate_transparency
copy.referrer_policy = @referrer_policy
copy.hpkp = @hpkp
copy.hpkp_report_host = @hpkp_report_host
Expand Down Expand Up @@ -200,6 +201,7 @@ def validate_config!
XDownloadOptions.validate_config!(@x_download_options)
XPermittedCrossDomainPolicies.validate_config!(@x_permitted_cross_domain_policies)
ClearSiteData.validate_config!(@clear_site_data)
ExpectCertificateTransparency.validate_config!(@expect_certificate_transparency)
PublicKeyPins.validate_config!(@hpkp)
Cookie.validate_config!(@cookies)
end
Expand Down
70 changes: 70 additions & 0 deletions lib/secure_headers/headers/expect_certificate_transparency.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true
module SecureHeaders
class ExpectCertificateTransparencyConfigError < StandardError; end

class ExpectCertificateTransparency
HEADER_NAME = "Expect-CT".freeze
CONFIG_KEY = :expect_certificate_transparency
INVALID_CONFIGURATION_ERROR = "config must be a hash.".freeze
INVALID_ENFORCE_VALUE_ERROR = "enforce must be a boolean".freeze
REQUIRED_MAX_AGE_ERROR = "max-age is a required directive.".freeze
INVALID_MAX_AGE_ERROR = "max-age must be a number.".freeze

class << self
# Public: Generate a Expect-CT header.
#
# Returns nil if not configured, returns header name and value if
# configured.
def make_header(config)
return if config.nil?

header = new(config)
[HEADER_NAME, header.value]
end

def validate_config!(config)
return if config.nil? || config == OPT_OUT
raise ExpectCertificateTransparencyConfigError.new(INVALID_CONFIGURATION_ERROR) unless config.is_a? Hash

unless [true, false, nil].include?(config[:enforce])
raise ExpectCertificateTransparencyConfigError.new(INVALID_ENFORCE_VALUE_ERROR)
end

if !config[:max_age]
raise ExpectCertificateTransparencyConfigError.new(REQUIRED_MAX_AGE_ERROR)
elsif config[:max_age].to_s !~ /\A\d+\z/
raise ExpectCertificateTransparencyConfigError.new(INVALID_MAX_AGE_ERROR)
end
end
end

def initialize(config)
@enforced = config.fetch(:enforce, nil)
@max_age = config.fetch(:max_age, nil)
@report_uri = config.fetch(:report_uri, nil)
end

def value
header_value = [
enforced_directive,
max_age_directive,
report_uri_directive
].compact.join("; ").strip
end

def enforced_directive
# Unfortunately `if @enforced` isn't enough here in case someone
# passes in a random string so let's be specific with it to prevent
# accidental enforcement.
"enforce" if @enforced == true
end

def max_age_directive
"max-age=#{@max_age}" if @max_age
end

def report_uri_directive
"report-uri=\"#{@report_uri}\"" if @report_uri
end
end
end
42 changes: 42 additions & 0 deletions spec/lib/secure_headers/headers/expect_certificate_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true
require "spec_helper"

module SecureHeaders
describe ExpectCertificateTransparency do
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: true).value).to eq("enforce; max-age=1234") }
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: false).value).to eq("max-age=1234") }
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, enforce: "yolocopter").value).to eq("max-age=1234") }
specify { expect(ExpectCertificateTransparency.new(max_age: 1234, report_uri: "https://report-uri.io/expect-ct").value).to eq("max-age=1234; report-uri=\"https://report-uri.io/expect-ct\"") }
specify do
config = { enforce: true, max_age: 1234, report_uri: "https://report-uri.io/expect-ct" }
header_value = "enforce; max-age=1234; report-uri=\"https://report-uri.io/expect-ct\""
expect(ExpectCertificateTransparency.new(config).value).to eq(header_value)
end

context "with an invalid configuration" do
it "raises an exception when configuration isn't a hash" do
expect do
ExpectCertificateTransparency.validate_config!(%w(a))
end.to raise_error(ExpectCertificateTransparencyConfigError)
end

it "raises an exception when max-age is not provided" do
expect do
ExpectCertificateTransparency.validate_config!(foo: "bar")
end.to raise_error(ExpectCertificateTransparencyConfigError)
end

it "raises an exception with an invalid max-age" do
expect do
ExpectCertificateTransparency.validate_config!(max_age: "abc123")
end.to raise_error(ExpectCertificateTransparencyConfigError)
end

it "raises an exception with an invalid enforce value" do
expect do
ExpectCertificateTransparency.validate_config!(enforce: "brokenstring")
end.to raise_error(ExpectCertificateTransparencyConfigError)
end
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def expect_default_values(hash)
expect(hash[SecureHeaders::XContentTypeOptions::HEADER_NAME]).to eq(SecureHeaders::XContentTypeOptions::DEFAULT_VALUE)
expect(hash[SecureHeaders::XPermittedCrossDomainPolicies::HEADER_NAME]).to eq(SecureHeaders::XPermittedCrossDomainPolicies::DEFAULT_VALUE)
expect(hash[SecureHeaders::ReferrerPolicy::HEADER_NAME]).to be_nil
expect(hash[SecureHeaders::ExpectCertificateTransparency::HEADER_NAME]).to be_nil
end

module SecureHeaders
Expand Down

0 comments on commit 0038e25

Please sign in to comment.