secure_headers-3.7.1/0000755000004100000410000000000013156767433014552 5ustar www-datawww-datasecure_headers-3.7.1/Rakefile0000644000004100000410000000104713156767433016221 0ustar www-datawww-data#!/usr/bin/env rake require 'bundler/gem_tasks' require 'rspec/core/rake_task' require 'net/http' require 'net/https' desc "Run RSpec" RSpec::Core::RakeTask.new do |t| t.verbose = false t.rspec_opts = "--format progress" end task default: :spec begin require 'rdoc/task' rescue LoadError require 'rdoc/rdoc' require 'rake/rdoctask' RDoc::Task = Rake::RDocTask end RDoc::Task.new(:rdoc) do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = 'SecureHeaders' rdoc.options << '--line-numbers' rdoc.rdoc_files.include('lib/**/*.rb') end secure_headers-3.7.1/Gemfile0000644000004100000410000000054513156767433016051 0ustar www-datawww-datasource "https://rubygems.org" gemspec group :test do gem "tins", "~> 1.6.0" # 1.7 requires ruby 2.0 gem "pry-nav" gem "json", "~> 1" gem "rack", "~> 1" gem "rspec" gem "coveralls" gem "term-ansicolor", "< 1.4" end group :guard do gem "guard-rspec", platforms: [:ruby_19, :ruby_20, :ruby_21, :ruby_22] gem "growl" gem "rb-fsevent" end secure_headers-3.7.1/secure_headers.gemspec0000644000004100000410000000161213156767433021100 0ustar www-datawww-data# -*- encoding: utf-8 -*- Gem::Specification.new do |gem| gem.name = "secure_headers" gem.version = "3.7.1" gem.authors = ["Neil Matatall"] gem.email = ["neil.matatall@gmail.com"] gem.description = 'Manages application of security headers with many safe defaults.' gem.summary = 'Add easily configured security headers to responses including content-security-policy, x-frame-options, strict-transport-security, etc.' gem.homepage = "https://github.com/twitter/secureheaders" gem.license = "Apache Public License 2.0" gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] gem.add_development_dependency "rake" gem.add_dependency "useragent" end secure_headers-3.7.1/.rspec0000644000004100000410000000003013156767433015660 0ustar www-datawww-data--order rand --warnings secure_headers-3.7.1/.ruby-version0000644000004100000410000000000613156767433017213 0ustar www-datawww-data2.3.3 secure_headers-3.7.1/CODE_OF_CONDUCT.md0000644000004100000410000000622413156767433017355 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neil.matatall@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ secure_headers-3.7.1/spec/0000755000004100000410000000000013156767433015504 5ustar www-datawww-datasecure_headers-3.7.1/spec/spec_helper.rb0000644000004100000410000000620213156767433020322 0ustar www-datawww-datarequire 'rubygems' require 'rspec' require 'rack' require 'pry-nav' require 'coveralls' Coveralls.wear! require File.join(File.dirname(__FILE__), '..', 'lib', 'secure_headers') USER_AGENTS = { edge: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", firefox: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:14.0) Gecko/20100101 Firefox/14.0.1", firefox46: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:46.0) Gecko/20100101 Firefox/46.0", chrome: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.56 Safari/536.5', ie: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/5.0)', opera: 'Opera/9.80 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00', ios5: "Mozilla/5.0 (iPhone; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3", ios6: "Mozilla/5.0 (iPhone; CPU iPhone OS 614 like Mac OS X) AppleWebKit/536.26 (KHTML like Gecko) Version/6.0 Mobile/10B350 Safari/8536.25", safari5: "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3", safari5_1: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", safari6: "Mozilla/5.0 (Macintosh; Intel Mac OS X 1084) AppleWebKit/536.30.1 (KHTML like Gecko) Version/6.0.5 Safari/536.30.1", safari10: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/602.2.11 (KHTML, like Gecko) Version/10.0.1 Safari/602.2.11" } def expect_default_values(hash) expect(hash[SecureHeaders::ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'") expect(hash[SecureHeaders::XFrameOptions::HEADER_NAME]).to eq(SecureHeaders::XFrameOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::XDownloadOptions::HEADER_NAME]).to eq(SecureHeaders::XDownloadOptions::DEFAULT_VALUE) expect(hash[SecureHeaders::StrictTransportSecurity::HEADER_NAME]).to eq(SecureHeaders::StrictTransportSecurity::DEFAULT_VALUE) expect(hash[SecureHeaders::XXssProtection::HEADER_NAME]).to eq(SecureHeaders::XXssProtection::DEFAULT_VALUE) 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 class Configuration class << self def clear_configurations @configurations = nil end end end end def reset_config SecureHeaders::Configuration.clear_configurations end def capture_warning begin old_stderr = $stderr $stderr = StringIO.new yield result = $stderr.string ensure $stderr = old_stderr end result end secure_headers-3.7.1/spec/lib/0000755000004100000410000000000013156767433016252 5ustar www-datawww-datasecure_headers-3.7.1/spec/lib/secure_headers_spec.rb0000644000004100000410000006127113156767433022601 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe SecureHeaders do before(:each) do reset_config end let(:request) { Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") } it "raises a NotYetConfiguredError if default has not been set" do expect do SecureHeaders.header_hash_for(request) end.to raise_error(Configuration::NotYetConfiguredError) end it "raises a NotYetConfiguredError if trying to opt-out of unconfigured headers" do expect do SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) end.to raise_error(Configuration::NotYetConfiguredError) end it "raises and ArgumentError when referencing an override that has not been set" do expect do Configuration.default SecureHeaders.use_secure_headers_override(request, :missing) end.to raise_error(ArgumentError) end describe "#header_hash_for" do it "allows you to opt out of individual headers via API" do Configuration.default do |config| config.csp = { default_src: %w('self')} config.csp_report_only = config.csp end SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyConfig::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY) SecureHeaders.opt_out_of_header(request, XContentTypeOptions::CONFIG_KEY) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to be_nil expect(hash['Content-Security-Policy']).to be_nil expect(hash['X-Content-Type-Options']).to be_nil end it "Carries options over when using overrides" do Configuration.default do |config| config.x_download_options = OPT_OUT config.x_permitted_cross_domain_policies = OPT_OUT end Configuration.override(:api) do |config| config.x_frame_options = OPT_OUT end SecureHeaders.use_secure_headers_override(request, :api) hash = SecureHeaders.header_hash_for(request) expect(hash['X-Download-Options']).to be_nil expect(hash['X-Permitted-Cross-Domain-Policies']).to be_nil expect(hash['X-Frame-Options']).to be_nil end it "allows you to opt out entirely" do # configure the disabled-by-default headers to ensure they also do not get set Configuration.default do |config| config.csp = { :default_src => ["example.com"] } config.csp_report_only = config.csp config.hpkp = { report_only: false, max_age: 10000000, include_subdomains: true, report_uri: "https://report-uri.io/example-hpkp", pins: [ {sha256: "abc"}, {sha256: "123"} ] } end SecureHeaders.opt_out_of_all_protection(request) hash = SecureHeaders.header_hash_for(request) ALL_HEADER_CLASSES.each do |klass| expect(hash[klass::CONFIG_KEY]).to be_nil end 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) hash = SecureHeaders.header_hash_for(request) expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::DENY) end it "allows you to override opting out" do Configuration.default do |config| config.x_frame_options = OPT_OUT config.csp = OPT_OUT end SecureHeaders.override_x_frame_options(request, XFrameOptions::SAMEORIGIN) SecureHeaders.override_content_security_policy_directives(request, default_src: %w(https:), script_src: %w('self')) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; script-src 'self'") expect(hash[XFrameOptions::HEADER_NAME]).to eq(XFrameOptions::SAMEORIGIN) end it "produces a UA-specific CSP when overriding (and busting the cache)" do Configuration.default do |config| config.csp = { default_src: %w('self'), child_src: %w('self') } end firefox_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:firefox])) # append an unsupported directive SecureHeaders.override_content_security_policy_directives(firefox_request, {plugin_types: %w(flash)}) # append a supported directive SecureHeaders.override_content_security_policy_directives(firefox_request, {script_src: %w('self')}) hash = SecureHeaders.header_hash_for(firefox_request) # child-src is translated to frame-src expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src 'self'; script-src 'self'") end it "produces a hash of headers with default config" do Configuration.default hash = SecureHeaders.header_hash_for(request) expect_default_values(hash) end it "does not set the HSTS header if request is over HTTP" do plaintext_request = Rack::Request.new({}) Configuration.default do |config| config.hsts = "max-age=123456" end expect(SecureHeaders.header_hash_for(plaintext_request)[StrictTransportSecurity::HEADER_NAME]).to be_nil end it "does not set the HPKP header if request is over HTTP" do plaintext_request = Rack::Request.new({}) Configuration.default do |config| config.hpkp = { max_age: 1_000_000, include_subdomains: true, report_uri: '//example.com/uri-directive', pins: [ { sha256: 'abc' }, { sha256: '123' } ] } end expect(SecureHeaders.header_hash_for(plaintext_request)[PublicKeyPins::HEADER_NAME]).to be_nil end context "content security policy" do let(:chrome_request) { Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:chrome])) } it "appends a value to csp directive" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com 'unsafe-inline') } end SecureHeaders.append_content_security_policy_directives(request, script_src: %w(anothercdn.com)) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline' anothercdn.com") end it "appends child-src to frame-src" do Configuration.default do |config| config.csp = { default_src: %w('self'), frame_src: %w(frame_src.com) } end SecureHeaders.append_content_security_policy_directives(chrome_request, child_src: %w(child_src.com)) hash = SecureHeaders.header_hash_for(chrome_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; child-src frame_src.com child_src.com") end it "appends frame-src to child-src" do Configuration.default do |config| config.csp = { default_src: %w('self'), child_src: %w(child_src.com) } end safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari6])) SecureHeaders.append_content_security_policy_directives(safari_request, frame_src: %w(frame_src.com)) hash = SecureHeaders.header_hash_for(safari_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; frame-src child_src.com frame_src.com") end it "supports named appends" do Configuration.default do |config| config.csp = { default_src: %w('self') } end Configuration.named_append(:moar_default_sources) do |request| { default_src: %w(https:)} end Configuration.named_append(:how_about_a_script_src_too) do |request| { script_src: %w('unsafe-inline')} end SecureHeaders.use_content_security_policy_named_append(request, :moar_default_sources) SecureHeaders.use_content_security_policy_named_append(request, :how_about_a_script_src_too) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self' https:; script-src 'self' https: 'unsafe-inline'") end it "appends a nonce to a missing script-src value" do Configuration.default do |config| config.csp = { default_src: %w('self') } end SecureHeaders.content_security_policy_script_nonce(request) # should add the value to the header hash = SecureHeaders.header_hash_for(chrome_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'nonce-.*'\z/) end it "appends a hash to a missing script-src value" do Configuration.default do |config| config.csp = { default_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, script_src: %w('sha256-abc123')) hash = SecureHeaders.header_hash_for(chrome_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/\Adefault-src 'self'; script-src 'self' 'sha256-abc123'\z/) end it "overrides individual directives" do Configuration.default do |config| config.csp = { default_src: %w('self') } end SecureHeaders.override_content_security_policy_directives(request, default_src: %w('none')) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'none'") end it "overrides non-existant directives" do Configuration.default do |config| config.csp = { default_src: %w(https:) } end SecureHeaders.override_content_security_policy_directives(request, img_src: [ContentSecurityPolicy::DATA_PROTOCOL]) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to be_nil expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src https:; img-src data:") end it "does not append a nonce when the browser does not support it" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com 'unsafe-inline'), style_src: %w('self') } end safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari5])) SecureHeaders.content_security_policy_script_nonce(safari_request) hash = SecureHeaders.header_hash_for(safari_request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to eq("default-src 'self'; script-src mycdn.com 'unsafe-inline'; style-src 'self'") end it "appends a nonce to the script-src when used" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com), style_src: %w('self') } end nonce = SecureHeaders.content_security_policy_script_nonce(chrome_request) # simulate the nonce being used multiple times in a request: SecureHeaders.content_security_policy_script_nonce(chrome_request) SecureHeaders.content_security_policy_script_nonce(chrome_request) SecureHeaders.content_security_policy_script_nonce(chrome_request) hash = SecureHeaders.header_hash_for(chrome_request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'; style-src 'self'") end it "uses a nonce for safari 10+" do Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(mycdn.com) } end safari_request = Rack::Request.new(request.env.merge("HTTP_USER_AGENT" => USER_AGENTS[:safari10])) nonce = SecureHeaders.content_security_policy_script_nonce(safari_request) hash = SecureHeaders.header_hash_for(safari_request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src mycdn.com 'nonce-#{nonce}'") end it "supports the deprecated `report_only: true` format" do expect(Kernel).to receive(:warn).once Configuration.default do |config| config.csp = { default_src: %w('self'), report_only: true } end expect(Configuration.get.csp).to eq(OPT_OUT) expect(Configuration.get.csp_report_only).to be_a(ContentSecurityPolicyReportOnlyConfig) hash = SecureHeaders.header_hash_for(request) expect(hash[ContentSecurityPolicyConfig::HEADER_NAME]).to be_nil expect(hash[ContentSecurityPolicyReportOnlyConfig::HEADER_NAME]).to eq("default-src 'self'") end it "Raises an error if csp_report_only is used with `report_only: false`" do expect do Configuration.default do |config| config.csp_report_only = { default_src: %w('self'), report_only: false } end end.to raise_error(ContentSecurityPolicyConfigError) end context "setting two headers" do before(:each) do Configuration.default do |config| config.csp = { default_src: %w('self') } config.csp_report_only = config.csp end end it "sets identical values when the configs are the same" do Configuration.default do |config| config.csp = { default_src: %w('self') } config.csp_report_only = { default_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "sets different headers when the configs are different" do Configuration.default do |config| config.csp = { default_src: %w('self') } config.csp_report_only = config.csp.merge({script_src: %w('self')}) end hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self'") end it "allows you to opt-out of enforced CSP" do Configuration.default do |config| config.csp = SecureHeaders::OPT_OUT config.csp_report_only = { default_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to be_nil expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "opts-out of enforced CSP when only csp_report_only is set" do expect(Kernel).to receive(:warn).once Configuration.default do |config| config.csp_report_only = { default_src: %w('self') } end hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to be_nil expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "allows you to set csp_report_only before csp" do expect(Kernel).to receive(:warn).once Configuration.default do |config| config.csp_report_only = { default_src: %w('self') } config.csp = config.csp_report_only.merge({script_src: %w('unsafe-inline')}) end hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' 'unsafe-inline'") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "allows appending to the enforced policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "allows appending to the report only policy" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows appending to both policies" do SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") end it "allows overriding the enforced policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :enforced) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'") end it "allows overriding the report only policy" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :report_only) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") end it "allows overriding both policies" do SecureHeaders.override_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}, :both) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src anothercdn.com") end context "when inferring which config to modify" do it "updates the enforced header when configured" do Configuration.default do |config| config.csp = { default_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to be_nil end it "updates the report only header when configured" do Configuration.default do |config| config.csp = OPT_OUT config.csp_report_only = { default_src: %w('self') } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src 'self'; script-src 'self' anothercdn.com") expect(hash['Content-Security-Policy']).to be_nil end it "updates both headers if both are configured" do Configuration.default do |config| config.csp = { default_src: %w(enforced.com) } config.csp_report_only = { default_src: %w(reportonly.com) } end SecureHeaders.append_content_security_policy_directives(request, {script_src: %w(anothercdn.com)}) hash = SecureHeaders.header_hash_for(request) expect(hash['Content-Security-Policy']).to eq("default-src enforced.com; script-src enforced.com anothercdn.com") expect(hash['Content-Security-Policy-Report-Only']).to eq("default-src reportonly.com; script-src reportonly.com anothercdn.com") end end end end end context "validation" do it "validates your hsts config upon configuration" do expect do Configuration.default do |config| config.hsts = 'lol' end end.to raise_error(STSConfigError) end it "validates your csp config upon configuration" do expect do Configuration.default do |config| config.csp = { ContentSecurityPolicy::DEFAULT_SRC => '123456' } end end.to raise_error(ContentSecurityPolicyConfigError) end it "raises errors for unknown directives" do expect do Configuration.default do |config| config.csp = { made_up_directive: '123456' } end end.to raise_error(ContentSecurityPolicyConfigError) end it "validates your xfo config upon configuration" do expect do Configuration.default do |config| config.x_frame_options = "NOPE" end end.to raise_error(XFOConfigError) end it "validates your xcto config upon configuration" do expect do Configuration.default do |config| config.x_content_type_options = "lol" end end.to raise_error(XContentTypeOptionsConfigError) end it "validates your clear site data config upon configuration" do expect do Configuration.default do |config| config.clear_site_data = 1 end end.to raise_error(ClearSiteDataConfigError) end it "validates your x_xss config upon configuration" do expect do Configuration.default do |config| config.x_xss_protection = "lol" end end.to raise_error(XXssProtectionConfigError) end it "validates your xdo config upon configuration" do expect do Configuration.default do |config| config.x_download_options = "lol" end end.to raise_error(XDOConfigError) end it "validates your x_permitted_cross_domain_policies config upon configuration" do expect do Configuration.default do |config| config.x_permitted_cross_domain_policies = "lol" end end.to raise_error(XPCDPConfigError) end it "validates your referrer_policy config upon configuration" do expect do Configuration.default do |config| config.referrer_policy = "lol" end end.to raise_error(ReferrerPolicyConfigError) end it "validates your hpkp config upon configuration" do expect do Configuration.default do |config| config.hpkp = "lol" end end.to raise_error(PublicKeyPinsConfigError) end it "validates your cookies config upon configuration" do expect do Configuration.default do |config| config.cookies = { secure: "lol" } end end.to raise_error(CookiesConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/0000755000004100000410000000000013156767433021233 5ustar www-datawww-datasecure_headers-3.7.1/spec/lib/secure_headers/view_helpers_spec.rb0000644000004100000410000000747413156767433025302 0ustar www-datawww-datarequire "spec_helper" require "erb" class Message < ERB include SecureHeaders::ViewHelpers def self.template <<-TEMPLATE <% hashed_javascript_tag(raise_error_on_unrecognized_hash = true) do %> console.log(1) <% end %> <% hashed_style_tag do %> body { background-color: black; } <% end %> <% nonced_javascript_tag do %> body { console.log(1) } <% end %> <% nonced_style_tag do %> body { background-color: black; } <% end %> TEMPLATE end def initialize(request, options = {}) @virtual_path = "/asdfs/index" @_request = request @template = self.class.template super(@template) end def capture(*args) yield(*args) end def content_tag(type, content = nil, options = nil, &block) content = if block_given? capture(block) end if options.is_a?(Hash) options = options.map {|k,v| " #{k}=#{v}"} end "<#{type}#{options}>#{content}" end def result super(binding) end def request @_request end end module SecureHeaders describe ViewHelpers do let(:app) { lambda { |env| [200, env, "app"] } } let(:middleware) { Middleware.new(app) } let(:request) { Rack::Request.new("HTTP_USER_AGENT" => USER_AGENTS[:chrome]) } let(:filename) { "app/views/asdfs/index.html.erb" } before(:all) do Configuration.default do |config| config.csp = { :default_src => %w('self'), :script_src => %w('self'), :style_src => %w('self') } end end after(:each) do Configuration.instance_variable_set(:@script_hashes, nil) Configuration.instance_variable_set(:@style_hashes, nil) end it "raises an error when using hashed content without precomputed hashes" do expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "raises an error when using hashed content with precomputed hashes, but none for the given file" do Configuration.instance_variable_set(:@script_hashes, filename.reverse => ["'sha256-123'"]) expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "raises an error when using previously unknown hashed content with precomputed hashes for a given file" do Configuration.instance_variable_set(:@script_hashes, filename => ["'sha256-123'"]) expect { Message.new(request).result }.to raise_error(ViewHelpers::UnexpectedHashedScriptException) end it "adds known hash values to the corresponding headers when the helper is used" do begin allow(SecureRandom).to receive(:base64).and_return("abc123") expected_hash = "sha256-3/URElR9+3lvLIouavYD/vhoICSNKilh15CzI/nKqg8=" Configuration.instance_variable_set(:@script_hashes, filename => ["'#{expected_hash}'"]) expected_style_hash = "sha256-7oYK96jHg36D6BM042er4OfBnyUDTG3pH1L8Zso3aGc=" Configuration.instance_variable_set(:@style_hashes, filename => ["'#{expected_style_hash}'"]) # render erb that calls out to helpers. Message.new(request).result _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'#{Regexp.escape(expected_hash)}'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/script-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'nonce-abc123'/) expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match(/style-src[^;]*'#{Regexp.escape(expected_style_hash)}'/) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/0000755000004100000410000000000013156767433022646 5ustar www-datawww-datasecure_headers-3.7.1/spec/lib/secure_headers/headers/x_download_options_spec.rb0000644000004100000410000000147713156767433030127 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe XDownloadOptions do specify { expect(XDownloadOptions.make_header).to eq([XDownloadOptions::HEADER_NAME, XDownloadOptions::DEFAULT_VALUE]) } specify { expect(XDownloadOptions.make_header('noopen')).to eq([XDownloadOptions::HEADER_NAME, 'noopen']) } context "invalid configuration values" do it "accepts noopen" do expect do XDownloadOptions.validate_config!("noopen") end.not_to raise_error end it "accepts nil" do expect do XDownloadOptions.validate_config!(nil) end.not_to raise_error end it "doesn't accept anything besides noopen" do expect do XDownloadOptions.validate_config!("open") end.to raise_error(XDOConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/referrer_policy_spec.rb0000644000004100000410000000377513156767433027414 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe ReferrerPolicy do specify { expect(ReferrerPolicy.make_header).to eq([ReferrerPolicy::HEADER_NAME, "origin-when-cross-origin"]) } specify { expect(ReferrerPolicy.make_header('no-referrer')).to eq([ReferrerPolicy::HEADER_NAME, "no-referrer"]) } context "valid configuration values" do it "accepts 'no-referrer'" do expect do ReferrerPolicy.validate_config!("no-referrer") end.not_to raise_error end it "accepts 'no-referrer-when-downgrade'" do expect do ReferrerPolicy.validate_config!("no-referrer-when-downgrade") end.not_to raise_error end it "accepts 'same-origin'" do expect do ReferrerPolicy.validate_config!("same-origin") end.not_to raise_error end it "accepts 'strict-origin'" do expect do ReferrerPolicy.validate_config!("strict-origin") end.not_to raise_error end it "accepts 'strict-origin-when-cross-origin'" do expect do ReferrerPolicy.validate_config!("strict-origin-when-cross-origin") end.not_to raise_error end it "accepts 'origin'" do expect do ReferrerPolicy.validate_config!("origin") end.not_to raise_error end it "accepts 'origin-when-cross-origin'" do expect do ReferrerPolicy.validate_config!("origin-when-cross-origin") end.not_to raise_error end it "accepts 'unsafe-url'" do expect do ReferrerPolicy.validate_config!("unsafe-url") end.not_to raise_error end it "accepts nil" do expect do ReferrerPolicy.validate_config!(nil) end.not_to raise_error end end context 'invlaid configuration values' do it "doesn't accept invalid values" do expect do ReferrerPolicy.validate_config!("open") end.to raise_error(ReferrerPolicyConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/x_frame_options_spec.rb0000644000004100000410000000202313156767433027376 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe XFrameOptions do describe "#value" do specify { expect(XFrameOptions.make_header).to eq([XFrameOptions::HEADER_NAME, XFrameOptions::DEFAULT_VALUE]) } specify { expect(XFrameOptions.make_header("DENY")).to eq([XFrameOptions::HEADER_NAME, "DENY"]) } context "with invalid configuration" do it "allows SAMEORIGIN" do expect do XFrameOptions.validate_config!("SAMEORIGIN") end.not_to raise_error end it "allows DENY" do expect do XFrameOptions.validate_config!("DENY") end.not_to raise_error end it "allows ALLOW-FROM*" do expect do XFrameOptions.validate_config!("ALLOW-FROM: example.com") end.not_to raise_error end it "does not allow garbage" do expect do XFrameOptions.validate_config!("I like turtles") end.to raise_error(XFOConfigError) end end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/strict_transport_security_spec.rb0000644000004100000410000000235413156767433031564 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe StrictTransportSecurity do describe "#value" do specify { expect(StrictTransportSecurity.make_header).to eq([StrictTransportSecurity::HEADER_NAME, StrictTransportSecurity::DEFAULT_VALUE]) } specify { expect(StrictTransportSecurity.make_header("max-age=1234; includeSubdomains; preload")).to eq([StrictTransportSecurity::HEADER_NAME, "max-age=1234; includeSubdomains; preload"]) } context "with an invalid configuration" do context "with a string argument" do it "raises an exception with an invalid max-age" do expect do StrictTransportSecurity.validate_config!('max-age=abc123') end.to raise_error(STSConfigError) end it "raises an exception if max-age is not supplied" do expect do StrictTransportSecurity.validate_config!('includeSubdomains') end.to raise_error(STSConfigError) end it "raises an exception with an invalid format" do expect do StrictTransportSecurity.validate_config!('max-age=123includeSubdomains') end.to raise_error(STSConfigError) end end end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/content_security_policy_spec.rb0000644000004100000410000003015113156767433031165 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe ContentSecurityPolicy do let (:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), style_src: %w('unsafe-inline' https: about:), report_uri: %w(/csp_report) } end describe "#name" do context "when in report-only mode" do specify { expect(ContentSecurityPolicy.new(default_opts.merge(report_only: true)).name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) } end context "when in enforce mode" do specify { expect(ContentSecurityPolicy.new(default_opts).name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) } end end describe "#value" do it "discards 'none' values if any other source expressions are present" do csp = ContentSecurityPolicy.new(default_opts.merge(child_src: %w('self' 'none'))) expect(csp.value).not_to include("'none'") end it "discards source expressions (besides unsafe-* and non-host source values) when * is present" do csp = ContentSecurityPolicy.new(default_src: %w(* 'unsafe-inline' 'unsafe-eval' http: https: example.org data: blob:)) expect(csp.value).to eq("default-src * 'unsafe-inline' 'unsafe-eval' data: blob:") end it "minifies source expressions based on overlapping wildcards" do config = { default_src: %w(a.example.org b.example.org *.example.org https://*.example.org) } csp = ContentSecurityPolicy.new(config) expect(csp.value).to eq("default-src *.example.org") end it "removes http/s schemes from hosts" do csp = ContentSecurityPolicy.new(default_src: %w(https://example.org)) expect(csp.value).to eq("default-src example.org") end it "does not remove schemes from report-uri values" do csp = ContentSecurityPolicy.new(default_src: %w(https:), report_uri: %w(https://example.org)) expect(csp.value).to eq("default-src https:; report-uri https://example.org") end it "does not remove schemes when :preserve_schemes is true" do csp = ContentSecurityPolicy.new(default_src: %w(https://example.org), :preserve_schemes => true) expect(csp.value).to eq("default-src https://example.org") end it "removes nil from source lists" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org", nil]) expect(csp.value).to eq("default-src example.org") end it "does not add a directive if the value is an empty array (or all nil)" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: [nil]) expect(csp.value).to eq("default-src example.org") end it "does not add a directive if the value is nil" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], script_src: nil) expect(csp.value).to eq("default-src example.org") end it "does add a boolean directive if the value is true" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: true) expect(csp.value).to eq("default-src example.org; block-all-mixed-content; upgrade-insecure-requests") end it "does not add a boolean directive if the value is false" do csp = ContentSecurityPolicy.new(default_src: ["https://example.org"], block_all_mixed_content: true, upgrade_insecure_requests: false) expect(csp.value).to eq("default-src example.org; block-all-mixed-content") end it "deduplicates any source expressions" do csp = ContentSecurityPolicy.new(default_src: %w(example.org example.org example.org)) expect(csp.value).to eq("default-src example.org") end it "creates maximally strict sandbox policy when passed no sandbox token values" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: []) expect(csp.value).to eq("default-src example.org; sandbox") end it "creates maximally strict sandbox policy when passed true" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: true) expect(csp.value).to eq("default-src example.org; sandbox") end it "creates sandbox policy when passed valid sandbox token values" do csp = ContentSecurityPolicy.new(default_src: %w(example.org), sandbox: %w(allow-forms allow-scripts)) expect(csp.value).to eq("default-src example.org; sandbox allow-forms allow-scripts") end it "does not emit a warning when using frame-src" do expect(Kernel).to_not receive(:warn) ContentSecurityPolicy.new(default_src: %w('self'), frame_src: %w('self')).value end it "emits a warning when child-src and frame-src are supplied but are not equal" do expect(Kernel).to receive(:warn).with(/both :child_src and :frame_src supplied and do not match./) ContentSecurityPolicy.new(default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)).value end it "will still set inconsistent child/frame-src values to be less surprising" do expect(Kernel).to receive(:warn).at_least(:once) firefox = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox]).value firefox_transitional = ContentSecurityPolicy.new({default_src: %w('self'), child_src: %w(child-src.com), frame_src: %w(frame-src,com)}, USER_AGENTS[:firefox46]).value expect(firefox).not_to eq(firefox_transitional) expect(firefox).to match(/frame-src/) expect(firefox).not_to match(/child-src/) expect(firefox_transitional).to match(/child-src/) expect(firefox_transitional).not_to match(/frame-src/) end it "supports strict-dynamic" do csp = ContentSecurityPolicy.new({default_src: %w('self'), script_src: [ContentSecurityPolicy::STRICT_DYNAMIC], script_nonce: 123456}, USER_AGENTS[:chrome]) expect(csp.value).to eq("default-src 'self'; script-src 'strict-dynamic' 'nonce-123456'") end context "browser sniffing" do let (:complex_opts) do (ContentSecurityPolicy::ALL_DIRECTIVES - [:frame_src]).each_with_object({}) do |directive, hash| hash[directive] = ["#{directive.to_s.gsub("_", "-")}.com"] end.merge({ block_all_mixed_content: true, upgrade_insecure_requests: true, script_src: %w(script-src.com), script_nonce: 123456, sandbox: %w(allow-forms), plugin_types: %w(application/pdf) }) end it "does not filter any directives for Chrome" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:chrome]) 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'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "does not filter any directives for Opera" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:opera]) 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'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, child-src, and plugin-types for firefox" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox]) 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'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end it "filters blocked-all-mixed-content, frame-src, and plugin-types for firefox 46 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:firefox46]) 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'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end 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, nonce sources, hash sources, and plugin-types for Edge" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:edge]) 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 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end 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, nonce sources, hash sources, and plugin-types for safari" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari6]) 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 'unsafe-inline'; style-src style-src.com; report-uri report-uri.com") end it "adds 'unsafe-inline', filters blocked-all-mixed-content, upgrade-insecure-requests, nonce sources, and hash sources for safari 10 and higher" do policy = ContentSecurityPolicy.new(complex_opts, USER_AGENTS[:safari10]) 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'; style-src style-src.com; report-uri report-uri.com") end it "falls back to standard Firefox defaults when the useragent version is not present" do ua = USER_AGENTS[:firefox].dup allow(ua).to receive(:version).and_return(nil) policy = ContentSecurityPolicy.new(complex_opts, ua) 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'; style-src style-src.com; upgrade-insecure-requests; report-uri report-uri.com") end end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/public_key_pins_spec.rb0000644000004100000410000000270313156767433027366 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe PublicKeyPins do specify { expect(PublicKeyPins.new(max_age: 1234, report_only: true).name).to eq("Public-Key-Pins-Report-Only") } specify { expect(PublicKeyPins.new(max_age: 1234).name).to eq("Public-Key-Pins") } specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } specify { expect(PublicKeyPins.new(max_age: 1234).value).to eq("max-age=1234") } specify do config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin1' }, { sha256: 'base64encodedpin2' }] } header_value = "max-age=1234; pin-sha256=\"base64encodedpin1\"; pin-sha256=\"base64encodedpin2\"" expect(PublicKeyPins.new(config).value).to eq(header_value) end context "with an invalid configuration" do it "raises an exception when max-age is not provided" do expect do PublicKeyPins.validate_config!(foo: 'bar') end.to raise_error(PublicKeyPinsConfigError) end it "raises an exception with an invalid max-age" do expect do PublicKeyPins.validate_config!(max_age: 'abc123') end.to raise_error(PublicKeyPinsConfigError) end it 'raises an exception with less than 2 pins' do expect do config = { max_age: 1234, pins: [{ sha256: 'base64encodedpin' }] } PublicKeyPins.validate_config!(config) end.to raise_error(PublicKeyPinsConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/x_permitted_cross_domain_policies_spec.rb0000644000004100000410000000271013156767433033160 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe XPermittedCrossDomainPolicies do specify { expect(XPermittedCrossDomainPolicies.make_header).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, "none"]) } specify { expect(XPermittedCrossDomainPolicies.make_header('master-only')).to eq([XPermittedCrossDomainPolicies::HEADER_NAME, 'master-only']) } context "valid configuration values" do it "accepts 'all'" do expect do XPermittedCrossDomainPolicies.validate_config!("all") end.not_to raise_error end it "accepts 'by-ftp-filename'" do expect do XPermittedCrossDomainPolicies.validate_config!("by-ftp-filename") end.not_to raise_error end it "accepts 'by-content-type'" do expect do XPermittedCrossDomainPolicies.validate_config!("by-content-type") end.not_to raise_error end it "accepts 'master-only'" do expect do XPermittedCrossDomainPolicies.validate_config!("master-only") end.not_to raise_error end it "accepts nil" do expect do XPermittedCrossDomainPolicies.validate_config!(nil) end.not_to raise_error end end context 'invlaid configuration values' do it "doesn't accept invalid values" do expect do XPermittedCrossDomainPolicies.validate_config!("open") end.to raise_error(XPCDPConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/policy_management_spec.rb0000644000004100000410000002167113156767433027707 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe PolicyManagement do let (:default_opts) do { default_src: %w(https:), img_src: %w(https: data:), script_src: %w('unsafe-inline' 'unsafe-eval' https: data:), style_src: %w('unsafe-inline' https: about:), report_uri: %w(/csp_report) } end describe "#validate_config!" do it "accepts all keys" do # (pulled from README) config = { # "meta" values. these will shaped the header, but the values are not included in the header. report_only: true, # default: false preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), frame_src: %w('self' *.twimg.com itunes.apple.com), child_src: %w('self' *.twimg.com itunes.apple.com), connect_src: %w(wss:), font_src: %w('self' data:), img_src: %w(mycdn.com data:), manifest_src: %w(manifest.com), media_src: %w(utoob.com), object_src: %w('self'), script_src: %w('self'), style_src: %w('unsafe-inline'), base_uri: %w('self'), form_action: %w('self' github.com), frame_ancestors: %w('none'), plugin_types: %w(application/x-shockwave-flash), block_all_mixed_content: true, # see [http://www.w3.org/TR/mixed-content/](http://www.w3.org/TR/mixed-content/) upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://example.com/uri-directive) } ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(config)) end it "requires a :default_src value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(script_src: %('self'))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :report_only to be a truthy value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(report_only: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :preserve_schemes to be a truthy value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(preserve_schemes: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :block_all_mixed_content to be a boolean value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(block_all_mixed_content: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires :upgrade_insecure_requests to be a boolean value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(upgrade_insecure_requests: "steve"))) end.to raise_error(ContentSecurityPolicyConfigError) end it "requires all source lists to be an array of strings" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end it "allows nil values" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), script_src: ["https:", nil])) end.to_not raise_error end it "rejects unknown directives / config" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w('self'), default_src_totally_mispelled: "steve")) end.to raise_error(ContentSecurityPolicyConfigError) end # this is mostly to ensure people don't use the antiquated shorthands common in other configs it "performs light validation on source lists" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_src: %w(self none inline eval))) end.to raise_error(ContentSecurityPolicyConfigError) end it "rejects anything not of the form allow-* as a sandbox value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["steve"]))) end.to raise_error(ContentSecurityPolicyConfigError) end it "accepts anything of the form allow-* as a sandbox value " do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: ["allow-foo"]))) end.to_not raise_error end it "accepts true as a sandbox policy" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(sandbox: true))) end.to_not raise_error end it "rejects anything not of the form type/subtype as a plugin-type value" do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["steve"]))) end.to raise_error(ContentSecurityPolicyConfigError) end it "accepts anything of the form type/subtype as a plugin-type value " do expect do ContentSecurityPolicy.validate_config!(ContentSecurityPolicyConfig.new(default_opts.merge(plugin_types: ["application/pdf"]))) end.to_not raise_error end end describe "#combine_policies" do it "combines the default-src value with the override if the directive was unconfigured" do Configuration.default do |config| config.csp = { default_src: %w(https:) } end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) csp = ContentSecurityPolicy.new(combined_config) expect(csp.name).to eq(ContentSecurityPolicyConfig::HEADER_NAME) expect(csp.value).to eq("default-src https:; script-src https: anothercdn.com") end it "combines directives where the original value is nil and the hash is frozen" do Configuration.default do |config| config.csp = { default_src: %w('self'), report_only: false }.freeze end report_uri = "https://report-uri.io/asdf" combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_uri: [report_uri]) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.value).to include("report-uri #{report_uri}") end it "does not combine the default-src value for directives that don't fall back to default sources" do Configuration.default do |config| config.csp = { default_src: %w('self'), report_only: false }.freeze end non_default_source_additions = ContentSecurityPolicy::NON_FETCH_SOURCES.each_with_object({}) do |directive, hash| hash[directive] = %w("http://example.org) end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, non_default_source_additions) ContentSecurityPolicy::NON_FETCH_SOURCES.each do |directive| expect(combined_config[directive]).to eq(%w("http://example.org)) end ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]).value end it "overrides the report_only flag" do Configuration.default do |config| config.csp = { default_src: %w('self'), report_only: false } end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, report_only: true) csp = ContentSecurityPolicy.new(combined_config, USER_AGENTS[:firefox]) expect(csp.name).to eq(ContentSecurityPolicyReportOnlyConfig::HEADER_NAME) end it "overrides the :block_all_mixed_content flag" do Configuration.default do |config| config.csp = { default_src: %w(https:), block_all_mixed_content: false } end combined_config = ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, block_all_mixed_content: true) csp = ContentSecurityPolicy.new(combined_config) expect(csp.value).to eq("default-src https:; block-all-mixed-content") end it "raises an error if appending to a OPT_OUT policy" do Configuration.default do |config| config.csp = OPT_OUT end expect do ContentSecurityPolicy.combine_policies(Configuration.get.csp.to_h, script_src: %w(anothercdn.com)) end.to raise_error(ContentSecurityPolicyConfigError) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/x_xss_protection_spec.rb0000644000004100000410000000321713156767433027622 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe XXssProtection do specify { expect(XXssProtection.make_header).to eq([XXssProtection::HEADER_NAME, XXssProtection::DEFAULT_VALUE]) } specify { expect(XXssProtection.make_header("1; mode=block; report=https://www.secure.com/reports")).to eq([XXssProtection::HEADER_NAME, '1; mode=block; report=https://www.secure.com/reports']) } context "with invalid configuration" do it "should raise an error when providing a string that is not valid" do expect do XXssProtection.validate_config!("asdf") end.to raise_error(XXssProtectionConfigError) expect do XXssProtection.validate_config!("asdf; mode=donkey") end.to raise_error(XXssProtectionConfigError) end context "when using a hash value" do it "should allow string values ('1' or '0' are the only valid strings)" do expect do XXssProtection.validate_config!('1') end.not_to raise_error end it "should raise an error if no value key is supplied" do expect do XXssProtection.validate_config!("mode=block") end.to raise_error(XXssProtectionConfigError) end it "should raise an error if an invalid key is supplied" do expect do XXssProtection.validate_config!("123") end.to raise_error(XXssProtectionConfigError) end it "should raise an error if mode != block" do expect do XXssProtection.validate_config!("1; mode=donkey") end.to raise_error(XXssProtectionConfigError) end end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/clear_site_data_spec.rb0000644000004100000410000000446613156767433027322 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe ClearSiteData do describe "make_header" do it "returns nil with nil config" do expect(described_class.make_header).to be_nil end it "returns nil with empty config" do expect(described_class.make_header([])).to be_nil end it "returns nil with opt-out config" do expect(described_class.make_header(OPT_OUT)).to be_nil end it "returns all types with `true` config" do name, value = described_class.make_header(true) expect(name).to eq(ClearSiteData::HEADER_NAME) expect(value).to eq( %("cache", "cookies", "storage", "executionContexts") ) end it "returns specified types" do name, value = described_class.make_header(["foo", "bar"]) expect(name).to eq(ClearSiteData::HEADER_NAME) expect(value).to eq(%("foo", "bar")) end end describe "validate_config!" do it "succeeds for `true` config" do expect do described_class.validate_config!(true) end.not_to raise_error end it "succeeds for `nil` config" do expect do described_class.validate_config!(nil) end.not_to raise_error end it "succeeds for opt-out config" do expect do described_class.validate_config!(OPT_OUT) end.not_to raise_error end it "succeeds for empty config" do expect do described_class.validate_config!([]) end.not_to raise_error end it "succeeds for Array of Strings config" do expect do described_class.validate_config!(["foo"]) end.not_to raise_error end it "fails for Array of non-String config" do expect do described_class.validate_config!([1]) end.to raise_error(ClearSiteDataConfigError) end it "fails for other types of config" do expect do described_class.validate_config!(:cookies) end.to raise_error(ClearSiteDataConfigError) end end describe "make_header_value" do it "returns a string of quoted values that are comma separated" do value = described_class.make_header_value(["foo", "bar"]) expect(value).to eq(%("foo", "bar")) end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/x_content_type_options_spec.rb0000644000004100000410000000167113156767433031027 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe XContentTypeOptions do describe "#value" do specify { expect(XContentTypeOptions.make_header).to eq([XContentTypeOptions::HEADER_NAME, XContentTypeOptions::DEFAULT_VALUE]) } specify { expect(XContentTypeOptions.make_header("nosniff")).to eq([XContentTypeOptions::HEADER_NAME, "nosniff"]) } context "invalid configuration values" do it "accepts nosniff" do expect do XContentTypeOptions.validate_config!("nosniff") end.not_to raise_error end it "accepts nil" do expect do XContentTypeOptions.validate_config!(nil) end.not_to raise_error end it "doesn't accept anything besides no-sniff" do expect do XContentTypeOptions.validate_config!("donkey") end.to raise_error(XContentTypeOptionsConfigError) end end end end end secure_headers-3.7.1/spec/lib/secure_headers/headers/expect_certificate_spec.rb0000644000004100000410000000366413156767433030050 0ustar www-datawww-data# 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 secure_headers-3.7.1/spec/lib/secure_headers/headers/cookie_spec.rb0000644000004100000410000001423213156767433025460 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe Cookie do let(:raw_cookie) { "_session=thisisatest" } it "does not tamper with cookies when unconfigured" do cookie = Cookie.new(raw_cookie, {}) expect(cookie.to_s).to eq(raw_cookie) end it "preserves existing attributes" do cookie = Cookie.new("_session=thisisatest; secure", secure: true) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "prevents duplicate flagging of attributes" do cookie = Cookie.new("_session=thisisatest; secure", secure: true) expect(cookie.to_s.scan(/secure/i).count).to eq(1) end context "Secure cookies" do context "when configured with a boolean" do it "flags cookies as Secure" do cookie = Cookie.new(raw_cookie, secure: true) expect(cookie.to_s).to eq("_session=thisisatest; secure") end end context "when configured with a Hash" do it "flags cookies as Secure when whitelisted" do cookie = Cookie.new(raw_cookie, secure: { only: ["_session"]}) expect(cookie.to_s).to eq("_session=thisisatest; secure") end it "does not flag cookies as Secure when excluded" do cookie = Cookie.new(raw_cookie, secure: { except: ["_session"] }) expect(cookie.to_s).to eq("_session=thisisatest") end end end context "HttpOnly cookies" do context "when configured with a boolean" do it "flags cookies as HttpOnly" do cookie = Cookie.new(raw_cookie, httponly: true) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end end context "when configured with a Hash" do it "flags cookies as HttpOnly when whitelisted" do cookie = Cookie.new(raw_cookie, httponly: { only: ["_session"]}) expect(cookie.to_s).to eq("_session=thisisatest; HttpOnly") end it "does not flag cookies as HttpOnly when excluded" do cookie = Cookie.new(raw_cookie, httponly: { except: ["_session"] }) expect(cookie.to_s).to eq("_session=thisisatest") end end end context "SameSite cookies" do it "flags SameSite=Lax" do cookie = Cookie.new(raw_cookie, samesite: { lax: { only: ["_session"] } }) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "flags SameSite=Lax when configured with a boolean" do cookie = Cookie.new(raw_cookie, samesite: { lax: true}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Lax") end it "does not flag cookies as SameSite=Lax when excluded" do cookie = Cookie.new(raw_cookie, samesite: { lax: { except: ["_session"] } }) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict" do cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] } }) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "does not flag cookies as SameSite=Strict when excluded" do cookie = Cookie.new(raw_cookie, samesite: { strict: { except: ["_session"] } }) expect(cookie.to_s).to eq("_session=thisisatest") end it "flags SameSite=Strict when configured with a boolean" do cookie = Cookie.new(raw_cookie, samesite: { strict: true}) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "flags properly when both lax and strict are configured" do raw_cookie = "_session=thisisatest" cookie = Cookie.new(raw_cookie, samesite: { strict: { only: ["_session"] }, lax: { only: ["_additional_session"] } }) expect(cookie.to_s).to eq("_session=thisisatest; SameSite=Strict") end it "ignores configuration if the cookie is already flagged" do raw_cookie = "_session=thisisatest; SameSite=Strict" cookie = Cookie.new(raw_cookie, samesite: { lax: true }) expect(cookie.to_s).to eq(raw_cookie) end end end context "with an invalid configuration" do it "raises an exception when not configured with a Hash" do expect do Cookie.validate_config!("configuration") end.to raise_error(CookiesConfigError) end it "raises an exception when configured without a boolean/Hash" do expect do Cookie.validate_config!(secure: "true") end.to raise_error(CookiesConfigError) end it "raises an exception when both only and except filters are provided" do expect do Cookie.validate_config!(secure: { only: [], except: [] }) end.to raise_error(CookiesConfigError) end it "raises an exception when SameSite is not configured with a Hash" do expect do Cookie.validate_config!(samesite: true) end.to raise_error(CookiesConfigError) end it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do expect do Cookie.validate_config!(samesite: { lax: true, strict: true}) end.to raise_error(CookiesConfigError) end it "raises an exception when SameSite lax and strict enforcement modes are configured with booleans" do expect do Cookie.validate_config!(samesite: { lax: true, strict: { only: ["_anything"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both only and except filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { only: ["_anything"], except: ["_anythingelse"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { only: ["_anything"] }, strict: { only: ["_anything"] } }) end.to raise_error(CookiesConfigError) end it "raises an exception when both lax and strict only filters are provided to SameSite configurations" do expect do Cookie.validate_config!(samesite: { lax: { except: ["_anything"] }, strict: { except: ["_anything"] } }) end.to raise_error(CookiesConfigError) end end end secure_headers-3.7.1/spec/lib/secure_headers/configuration_spec.rb0000644000004100000410000000566713156767433025457 0ustar www-datawww-datarequire 'spec_helper' module SecureHeaders describe Configuration do before(:each) do reset_config Configuration.default end it "has a default config" do expect(Configuration.get(Configuration::DEFAULT_CONFIG)).to_not be_nil end it "has an 'noop' config" do expect(Configuration.get(Configuration::NOOP_CONFIGURATION)).to_not be_nil end it "precomputes headers upon creation" do default_config = Configuration.get(Configuration::DEFAULT_CONFIG) header_hash = default_config.cached_headers.each_with_object({}) do |(key, value), hash| header_name, header_value = if key == :csp value["Chrome"] else value end hash[header_name] = header_value end expect_default_values(header_hash) end it "copies config values when duping" do Configuration.override(:test_override, Configuration::NOOP_CONFIGURATION) do # do nothing, just copy it end config = Configuration.get(:test_override) noop = Configuration.get(Configuration::NOOP_CONFIGURATION) [:csp, :csp_report_only, :cookies].each do |key| expect(config.send(key)).to eq(noop.send(key)) end end it "regenerates cached headers when building an override" do Configuration.override(:test_override) do |config| config.x_content_type_options = OPT_OUT end expect(Configuration.get.cached_headers).to_not eq(Configuration.get(:test_override).cached_headers) end it "stores an override of the global config" do Configuration.override(:test_override) do |config| config.x_frame_options = "DENY" end expect(Configuration.get(:test_override)).to_not be_nil end it "deep dup's config values when overriding so the original cannot be modified" do Configuration.override(:override) do |config| config.csp[:default_src] << "'self'" end default = Configuration.get override = Configuration.get(:override) expect(override.csp.directive_value(:default_src)).not_to be(default.csp.directive_value(:default_src)) end it "allows you to override an override" do Configuration.override(:override) do |config| config.csp = { default_src: %w('self')} end Configuration.override(:second_override, :override) do |config| config.csp = config.csp.merge(script_src: %w(example.org)) end original_override = Configuration.get(:override) expect(original_override.csp.to_h).to eq(default_src: %w('self')) override_config = Configuration.get(:second_override) expect(override_config.csp.to_h).to eq(default_src: %w('self'), script_src: %w('self' example.org)) end it "deprecates the secure_cookies configuration" do expect(Kernel).to receive(:warn).with(/\[DEPRECATION\]/) Configuration.default do |config| config.secure_cookies = true end end end end secure_headers-3.7.1/spec/lib/secure_headers/middleware_spec.rb0000644000004100000410000001102013156767433024701 0ustar www-datawww-datarequire "spec_helper" module SecureHeaders describe Middleware do let(:app) { lambda { |env| [200, env, "app"] } } let(:cookie_app) { lambda { |env| [200, env.merge("Set-Cookie" => "foo=bar"), "app"] } } let(:middleware) { Middleware.new(app) } let(:cookie_middleware) { Middleware.new(cookie_app) } before(:each) do reset_config Configuration.default end it "warns if the hpkp report-uri host is the same as the current host" do report_host = "report-uri.io" Configuration.default do |config| config.hpkp = { max_age: 10000000, pins: [ {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], report_uri: "https://#{report_host}/example-hpkp" } end expect(Kernel).to receive(:warn).with(Middleware::HPKP_SAME_HOST_WARNING) middleware.call(Rack::MockRequest.env_for("https://#{report_host}", {})) end it "sets the headers" do _, env = middleware.call(Rack::MockRequest.env_for("https://looocalhost", {})) expect_default_values(env) end it "respects overrides" do request = Rack::Request.new("HTTP_X_FORWARDED_SSL" => "on") SecureHeaders.override_x_frame_options(request, "DENY") _, env = middleware.call request.env expect(env[XFrameOptions::HEADER_NAME]).to eq("DENY") end it "uses named overrides" do Configuration.override("my_custom_config") do |config| config.csp[:script_src] = %w(example.org) end request = Rack::Request.new({}) SecureHeaders.use_secure_headers_override(request, "my_custom_config") expect(request.env[SECURE_HEADERS_CONFIG]).to be(Configuration.get("my_custom_config")) _, env = middleware.call request.env expect(env[ContentSecurityPolicyConfig::HEADER_NAME]).to match("example.org") end context "secure_cookies" do context "cookies should be flagged" do it "flags cookies as secure" do capture_warning do Configuration.default { |config| config.secure_cookies = true } end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar; secure") end end context "cookies should not be flagged" do it "does not flags cookies as secure" do capture_warning do Configuration.default { |config| config.secure_cookies = false } end request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar") end end end context "cookies" do it "flags cookies from configuration" do Configuration.default { |config| config.cookies = { secure: true, httponly: true } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar; secure; HttpOnly") end it "flags cookies with a combination of SameSite configurations" do cookie_middleware = Middleware.new(lambda { |env| [200, env.merge("Set-Cookie" => ["_session=foobar", "_guest=true"]), "app"] }) Configuration.default { |config| config.cookies = { samesite: { lax: { except: ["_session"] }, strict: { only: ["_session"] } } } } request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to match("_session=foobar; SameSite=Strict") expect(env['Set-Cookie']).to match("_guest=true; SameSite=Lax") end it "disables secure cookies for non-https requests" do Configuration.default { |config| config.cookies = { secure: true } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar") end it "sets the secure cookie flag correctly on interleaved http/https requests" do Configuration.default { |config| config.cookies = { secure: true } } request = Rack::Request.new("HTTPS" => "off") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar") request = Rack::Request.new("HTTPS" => "on") _, env = cookie_middleware.call request.env expect(env['Set-Cookie']).to eq("foo=bar; secure") end end end end secure_headers-3.7.1/.travis.yml0000644000004100000410000000044413156767433016665 0ustar www-datawww-datalanguage: ruby rvm: - ruby-head - 2.4.0 - 2.3.3 - 2.2 - 2.1 - 2.0.0 - 1.9.3 - jruby-19mode - jruby-head matrix: allow_failures: - rvm: jruby-head - rvm: ruby-head before_install: gem update bundler bundler_args: --without guard -j 3 sudo: false cache: bundler secure_headers-3.7.1/lib/0000755000004100000410000000000013156767433015320 5ustar www-datawww-datasecure_headers-3.7.1/lib/secure_headers.rb0000644000004100000410000002715013156767433020633 0ustar www-datawww-datarequire "secure_headers/configuration" require "secure_headers/hash_helper" require "secure_headers/headers/cookie" require "secure_headers/headers/public_key_pins" require "secure_headers/headers/content_security_policy" require "secure_headers/headers/x_frame_options" require "secure_headers/headers/strict_transport_security" require "secure_headers/headers/x_xss_protection" require "secure_headers/headers/x_content_type_options" require "secure_headers/headers/x_download_options" 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" require "useragent" require "singleton" # All headers (except for hpkp) have a default value. Provide SecureHeaders::OPT_OUT # or ":optout_of_protection" as a config value to disable a given header module SecureHeaders class NoOpHeaderConfig include Singleton def boom(arg = nil) raise "Illegal State: attempted to modify NoOpHeaderConfig. Create a new config instead." end def to_h {} end def dup self.class.instance end def opt_out? true end alias_method :[], :boom alias_method :[]=, :boom alias_method :keys, :boom end OPT_OUT = NoOpHeaderConfig.instance SECURE_HEADERS_CONFIG = "secure_headers_request_config".freeze NONCE_KEY = "secure_headers_content_security_policy_nonce".freeze HTTPS = "https".freeze CSP = ContentSecurityPolicy ALL_HEADER_CLASSES = [ ExpectCertificateTransparency, ClearSiteData, ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig, StrictTransportSecurity, PublicKeyPins, ReferrerPolicy, XContentTypeOptions, XDownloadOptions, XFrameOptions, XPermittedCrossDomainPolicies, XXssProtection ].freeze ALL_HEADERS_BESIDES_CSP = ( ALL_HEADER_CLASSES - [ContentSecurityPolicyConfig, ContentSecurityPolicyReportOnlyConfig] ).freeze # Headers set on http requests (excludes STS and HPKP) HTTP_HEADER_CLASSES = (ALL_HEADER_CLASSES - [StrictTransportSecurity, PublicKeyPins]).freeze class << self # Public: override a given set of directives for the current request. If a # value already exists for a given directive, it will be overridden. # # If CSP was previously OPT_OUT, a new blank policy is used. # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def override_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) if [:both, :enforced].include?(target) if config.csp.opt_out? config.csp = ContentSecurityPolicyConfig.new({}) end config.csp.merge!(additions) end if [:both, :report_only].include?(target) if config.csp_report_only.opt_out? config.csp_report_only = ContentSecurityPolicyReportOnlyConfig.new({}) end config.csp_report_only.merge!(additions) end override_secure_headers_request_config(request, config) end # Public: appends source values to the current configuration. If no value # is set for a given directive, the value will be merged with the default-src # value. If a value exists for the given directive, the values will be combined. # # additions - a hash containing directives. e.g. # script_src: %w(another-host.com) def append_content_security_policy_directives(request, additions, target = nil) config, target = config_and_target(request, target) if [:both, :enforced].include?(target) && !config.csp.opt_out? config.csp.append(additions) end if [:both, :report_only].include?(target) && !config.csp_report_only.opt_out? config.csp_report_only.append(additions) end override_secure_headers_request_config(request, config) end def use_content_security_policy_named_append(request, name) additions = SecureHeaders::Configuration.named_appends(name).call(request) append_content_security_policy_directives(request, additions) end # Public: override X-Frame-Options settings for this request. # # value - deny, sameorigin, or allowall # # Returns the current config def override_x_frame_options(request, value) config = config_for(request) config.update_x_frame_options(value) override_secure_headers_request_config(request, config) end # Public: opts out of setting a given header by creating a temporary config # and setting the given headers config to OPT_OUT. def opt_out_of_header(request, header_key) config = config_for(request) config.opt_out(header_key) override_secure_headers_request_config(request, config) end # Public: opts out of setting all headers by telling secure_headers to use # the NOOP configuration. def opt_out_of_all_protection(request) use_secure_headers_override(request, Configuration::NOOP_CONFIGURATION) end # Public: Builds the hash of headers that should be applied base on the # request. # # StrictTransportSecurity and PublicKeyPins are not applied to http requests. # See #config_for to determine which config is used for a given request. # # Returns a hash of header names => header values. The value # returned is meant to be merged into the header value from `@app.call(env)` # in Rack middleware. def header_hash_for(request) prevent_dup = true config = config_for(request, prevent_dup) headers = config.cached_headers user_agent = UserAgent.parse(request.user_agent) if !config.csp.opt_out? && config.csp.modified? headers = update_cached_csp(config.csp, headers, user_agent) end if !config.csp_report_only.opt_out? && config.csp_report_only.modified? headers = update_cached_csp(config.csp_report_only, headers, user_agent) end header_classes_for(request).each_with_object({}) do |klass, hash| if header = headers[klass::CONFIG_KEY] header_name, value = if klass == ContentSecurityPolicyConfig || klass == ContentSecurityPolicyReportOnlyConfig csp_header_for_ua(header, user_agent) else header end hash[header_name] = value end end end # Public: specify which named override will be used for this request. # Raises an argument error if no named override exists. # # name - the name of the previously configured override. def use_secure_headers_override(request, name) if config = Configuration.get(name) override_secure_headers_request_config(request, config) else raise ArgumentError.new("no override by the name of #{name} has been configured") end end # Public: gets or creates a nonce for CSP. # # The nonce will be added to script_src # # Returns the nonce def content_security_policy_script_nonce(request) content_security_policy_nonce(request, ContentSecurityPolicy::SCRIPT_SRC) end # Public: gets or creates a nonce for CSP. # # The nonce will be added to style_src # # Returns the nonce def content_security_policy_style_nonce(request) content_security_policy_nonce(request, ContentSecurityPolicy::STYLE_SRC) end # Public: Retreives the config for a given header type: # # Checks to see if there is an override for this request, then # Checks to see if a named override is used for this request, then # Falls back to the global config def config_for(request, prevent_dup = false) config = request.env[SECURE_HEADERS_CONFIG] || Configuration.get(Configuration::DEFAULT_CONFIG) # Global configs are frozen, per-request configs are not. When we're not # making modifications to the config, prevent_dup ensures we don't dup # the object unnecessarily. It's not necessarily frozen to begin with. if config.frozen? && !prevent_dup config.dup else config end end private TARGETS = [:both, :enforced, :report_only] def raise_on_unknown_target(target) unless TARGETS.include?(target) raise "Unrecognized target: #{target}. Must be [:both, :enforced, :report_only]" end end def config_and_target(request, target) config = config_for(request) target = guess_target(config) unless target raise_on_unknown_target(target) [config, target] end def guess_target(config) if !config.csp.opt_out? && !config.csp_report_only.opt_out? :both elsif !config.csp.opt_out? :enforced elsif !config.csp_report_only.opt_out? :report_only else :both end end # Private: gets or creates a nonce for CSP. # # Returns the nonce def content_security_policy_nonce(request, script_or_style) request.env[NONCE_KEY] ||= SecureRandom.base64(32).chomp nonce_key = script_or_style == ContentSecurityPolicy::SCRIPT_SRC ? :script_nonce : :style_nonce append_content_security_policy_directives(request, nonce_key => request.env[NONCE_KEY]) request.env[NONCE_KEY] end # Private: convenience method for specifying which configuration object should # be used for this request. # # Returns the config. def override_secure_headers_request_config(request, config) request.env[SECURE_HEADERS_CONFIG] = config end # Private: determines which headers are applicable to a given request. # # Returns a list of classes whose corresponding header values are valid for # this request. def header_classes_for(request) if request.scheme == HTTPS ALL_HEADER_CLASSES else HTTP_HEADER_CLASSES end end def update_cached_csp(config, headers, user_agent) headers = Configuration.send(:deep_copy, headers) headers[config.class::CONFIG_KEY] = {} variation = ContentSecurityPolicy.ua_to_variation(user_agent) headers[config.class::CONFIG_KEY][variation] = ContentSecurityPolicy.make_header(config, user_agent) headers end # Private: chooses the applicable CSP header for the provided user agent. # # headers - a hash of header_config_key => [header_name, header_value] # # Returns a CSP [header, value] array def csp_header_for_ua(headers, user_agent) headers[ContentSecurityPolicy.ua_to_variation(user_agent)] end end # These methods are mixed into controllers and delegate to the class method # with the same name. def use_secure_headers_override(name) SecureHeaders.use_secure_headers_override(request, name) end def content_security_policy_script_nonce SecureHeaders.content_security_policy_script_nonce(request) end def content_security_policy_style_nonce SecureHeaders.content_security_policy_style_nonce(request) end def opt_out_of_header(header_key) SecureHeaders.opt_out_of_header(request, header_key) end def append_content_security_policy_directives(additions) SecureHeaders.append_content_security_policy_directives(request, additions) end def override_content_security_policy_directives(additions) SecureHeaders.override_content_security_policy_directives(request, additions) end def override_x_frame_options(value) SecureHeaders.override_x_frame_options(request, value) end def use_content_security_policy_named_append(name) SecureHeaders.use_content_security_policy_named_append(request, name) end end secure_headers-3.7.1/lib/tasks/0000755000004100000410000000000013156767433016445 5ustar www-datawww-datasecure_headers-3.7.1/lib/tasks/tasks.rake0000644000004100000410000000506513156767433020444 0ustar www-datawww-dataINLINE_SCRIPT_REGEX = /()(.*?)<\/script>/mx unless defined? INLINE_SCRIPT_REGEX INLINE_STYLE_REGEX = /(]*>)(.*?)<\/style>/mx unless defined? INLINE_STYLE_REGEX INLINE_HASH_SCRIPT_HELPER_REGEX = /<%=\s?hashed_javascript_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_SCRIPT_HELPER_REGEX INLINE_HASH_STYLE_HELPER_REGEX = /<%=\s?hashed_style_tag(.*?)\s+do\s?%>(.*?)<%\s*end\s*%>/mx unless defined? INLINE_HASH_STYLE_HELPER_REGEX namespace :secure_headers do include SecureHeaders::HashHelper def is_erb?(filename) filename =~ /\.erb\Z/ end def is_mustache?(filename) filename =~ /\.mustache\Z/ end def dynamic_content?(filename, inline_script) (is_mustache?(filename) && inline_script =~ /\{\{.*\}\}/) || (is_erb?(filename) && inline_script =~ /<%.*%>/) end def find_inline_content(filename, regex, hashes) file = File.read(filename) file.scan(regex) do # TODO don't use gsub inline_script = Regexp.last_match.captures.last if dynamic_content?(filename, inline_script) puts "Looks like there's some dynamic content inside of a tag :-/" puts "That pretty much means the hash value will never match." puts "Code: " + inline_script puts "=" * 20 end hashes << hash_source(inline_script) end end def generate_inline_script_hashes(filename) hashes = [] [INLINE_SCRIPT_REGEX, INLINE_HASH_SCRIPT_HELPER_REGEX].each do |regex| find_inline_content(filename, regex, hashes) end hashes end def generate_inline_style_hashes(filename) hashes = [] [INLINE_STYLE_REGEX, INLINE_HASH_STYLE_HELPER_REGEX].each do |regex| find_inline_content(filename, regex, hashes) end hashes end desc "Generate #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" task :generate_hashes do |t, args| script_hashes = { "scripts" => {}, "styles" => {} } Dir.glob("app/{views,templates}/**/*.{erb,mustache}") do |filename| hashes = generate_inline_script_hashes(filename) if hashes.any? script_hashes["scripts"][filename] = hashes end hashes = generate_inline_style_hashes(filename) if hashes.any? script_hashes["styles"][filename] = hashes end end File.open(SecureHeaders::Configuration::HASH_CONFIG_FILE, 'w') do |file| file.write(script_hashes.to_yaml) end puts "Script hashes from " + script_hashes.keys.size.to_s + " files added to #{SecureHeaders::Configuration::HASH_CONFIG_FILE}" end end secure_headers-3.7.1/lib/secure_headers/0000755000004100000410000000000013156767433020301 5ustar www-datawww-datasecure_headers-3.7.1/lib/secure_headers/view_helper.rb0000644000004100000410000000774313156767433023152 0ustar www-datawww-datamodule SecureHeaders module ViewHelpers include SecureHeaders::HashHelper SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes" class UnexpectedHashedScriptException < StandardError; end # Public: create a style tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe style tag with the nonce attribute. def nonced_style_tag(content_or_options = {}, &block) nonced_tag(:style, content_or_options, block) end # Public: create a script tag using the content security policy nonce. # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns an html-safe script tag with the nonce attribute. def nonced_javascript_tag(content_or_options = {}, &block) nonced_tag(:script, content_or_options, block) end # Public: use the content security policy nonce for this request directly. # Instructs secure_headers to append a nonce to style/script-src directives. # # Returns a non-html-safe nonce value. def content_security_policy_nonce(type) case type when :script SecureHeaders.content_security_policy_script_nonce(@_request) when :style SecureHeaders.content_security_policy_style_nonce(@_request) end end def content_security_policy_script_nonce content_security_policy_nonce(:script) end def content_security_policy_style_nonce content_security_policy_nonce(:style) end ## # Checks to see if the hashed code is expected and adds the hash source # value to the current CSP. # # By default, in development/test/etc. an exception will be raised. def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block) hashed_tag( :script, :script_src, Configuration.instance_variable_get(:@script_hashes), raise_error_on_unrecognized_hash, block ) end def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block) hashed_tag( :style, :style_src, Configuration.instance_variable_get(:@style_hashes), raise_error_on_unrecognized_hash, block ) end private def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block) if raise_error_on_unrecognized_hash.nil? raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production" end content = capture(&block) file_path = File.join('app', 'views', self.instance_variable_get(:@virtual_path) + '.html.erb') if raise_error_on_unrecognized_hash hash_value = hash_source(content) message = unexpected_hash_error_message(file_path, content, hash_value) if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value) raise UnexpectedHashedScriptException.new(message) end end SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path]) content_tag type, content end def unexpected_hash_error_message(file_path, content, hash_value) <<-EOF \n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} *** #{content} *** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:*** #{file_path}: - \"#{hash_value}\"\n\n NOTE: dynamic javascript is not supported using script hash integration on purpose. It defeats the point of using it in the first place. EOF end def nonced_tag(type, content_or_options, block) options = {} content = if block options = content_or_options capture(&block) else content_or_options.html_safe # :'( end content_tag type, content, options.merge(nonce: content_security_policy_nonce(type)) end end end ActiveSupport.on_load :action_view do include SecureHeaders::ViewHelpers end if defined?(ActiveSupport) secure_headers-3.7.1/lib/secure_headers/configuration.rb0000644000004100000410000002612713156767433023505 0ustar www-datawww-datarequire 'yaml' module SecureHeaders class Configuration DEFAULT_CONFIG = :default NOOP_CONFIGURATION = "secure_headers_noop_config" class NotYetConfiguredError < StandardError; end class IllegalPolicyModificationError < StandardError; end class << self # Public: Set the global default configuration. # # Optionally supply a block to override the defaults set by this library. # # Returns the newly created config. def default(&block) config = new(&block) add_noop_configuration add_configuration(DEFAULT_CONFIG, config) end alias_method :configure, :default # Public: create a named configuration that overrides the default config. # # name - use an idenfier for the override config. # base - override another existing config, or override the default config # if no value is supplied. # # Returns: the newly created config def override(name, base = DEFAULT_CONFIG, &block) unless get(base) raise NotYetConfiguredError, "#{base} policy not yet supplied" end override = @configurations[base].dup override.instance_eval(&block) if block_given? add_configuration(name, override) end # Public: retrieve a global configuration object # # Returns the configuration with a given name or raises a # NotYetConfiguredError if `default` has not been called. def get(name = DEFAULT_CONFIG) if @configurations.nil? raise NotYetConfiguredError, "Default policy not yet supplied" end @configurations[name] end def named_appends(name) @appends ||= {} @appends[name] end def named_append(name, target = nil, &block) @appends ||= {} raise "Provide a configuration block" unless block_given? @appends[name] = block end private # Private: add a valid configuration to the global set of named configs. # # config - the config to store # name - the lookup value for this config # # Raises errors if the config is invalid or if a config named `name` # already exists. # # Returns the config, if valid def add_configuration(name, config) config.validate_config! @configurations ||= {} config.send(:cache_headers!) config.send(:cache_hpkp_report_host) config.freeze @configurations[name] = config end # Private: Automatically add an "opt-out of everything" override. # # Returns the noop config def add_noop_configuration noop_config = new do |config| ALL_HEADER_CLASSES.each do |klass| config.send("#{klass::CONFIG_KEY}=", OPT_OUT) end end add_configuration(NOOP_CONFIGURATION, noop_config) end # Public: perform a basic deep dup. The shallow copy provided by dup/clone # can lead to modifying parent objects. def deep_copy(config) return unless config config.each_with_object({}) do |(key, value), hash| hash[key] = if value.is_a?(Array) value.dup else value end end end # Private: convenience method purely DRY things up. The value may not be a # hash (e.g. OPT_OUT, nil) def deep_copy_if_hash(value) if value.is_a?(Hash) deep_copy(value) else value end end end 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, :expect_certificate_transparency attr_reader :cached_headers, :csp, :cookies, :csp_report_only, :hpkp, :hpkp_report_host @script_hashes = nil @style_hashes = nil HASH_CONFIG_FILE = ENV["secure_headers_generated_hashes_file"] || "config/secure_headers_generated_hashes.yml" if File.exist?(HASH_CONFIG_FILE) config = YAML.safe_load(File.open(HASH_CONFIG_FILE)) @script_hashes = config["scripts"] @style_hashes = config["styles"] end def initialize(&block) @cookies = nil @clear_site_data = nil @csp = nil @csp_report_only = nil @hpkp_report_host = nil @hpkp = nil @hsts = nil @x_content_type_options = nil @x_download_options = nil @x_frame_options = nil @x_permitted_cross_domain_policies = nil @x_xss_protection = nil @expect_certificate_transparency = nil self.hpkp = OPT_OUT self.referrer_policy = OPT_OUT self.csp = ContentSecurityPolicyConfig.new(ContentSecurityPolicyConfig::DEFAULT) self.csp_report_only = OPT_OUT instance_eval(&block) if block_given? end # Public: copy everything but the cached headers # # Returns a deep-dup'd copy of this configuration. def dup copy = self.class.new copy.cookies = self.class.send(:deep_copy_if_hash, @cookies) copy.csp = @csp.dup if @csp copy.csp_report_only = @csp_report_only.dup if @csp_report_only copy.cached_headers = self.class.send(:deep_copy_if_hash, @cached_headers) copy.x_content_type_options = @x_content_type_options copy.hsts = @hsts copy.x_frame_options = @x_frame_options copy.x_xss_protection = @x_xss_protection 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 copy end def opt_out(header) send("#{header}=", OPT_OUT) self.cached_headers.delete(header) end def update_x_frame_options(value) @x_frame_options = value self.cached_headers[XFrameOptions::CONFIG_KEY] = XFrameOptions.make_header(value) end # Public: validates all configurations values. # # Raises various configuration errors if any invalid config is detected. # # Returns nothing def validate_config! StrictTransportSecurity.validate_config!(@hsts) ContentSecurityPolicy.validate_config!(@csp) ContentSecurityPolicy.validate_config!(@csp_report_only) ReferrerPolicy.validate_config!(@referrer_policy) XFrameOptions.validate_config!(@x_frame_options) XContentTypeOptions.validate_config!(@x_content_type_options) XXssProtection.validate_config!(@x_xss_protection) 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 def secure_cookies=(secure_cookies) Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#secure_cookies=` is deprecated. Please use `#cookies=` to configure secure cookies instead." @cookies = (@cookies || {}).merge(secure: secure_cookies) end def csp=(new_csp) if new_csp.respond_to?(:opt_out?) @csp = new_csp.dup else if new_csp[:report_only] # Deprecated configuration implies that CSPRO should be set, CSP should not - so opt out Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp=` was supplied a config with report_only: true. Use #csp_report_only=" @csp = OPT_OUT self.csp_report_only = new_csp else @csp = ContentSecurityPolicyConfig.new(new_csp) end end end # Configures the Content-Security-Policy-Report-Only header. `new_csp` cannot # contain `report_only: false` or an error will be raised. # # NOTE: if csp has not been configured/has the default value when # configuring csp_report_only, the code will assume you mean to only use # report-only mode and you will be opted-out of enforce mode. def csp_report_only=(new_csp) @csp_report_only = begin if new_csp.is_a?(ContentSecurityPolicyConfig) new_csp.make_report_only elsif new_csp.respond_to?(:opt_out?) new_csp.dup else if new_csp[:report_only] == false # nil is a valid value on which we do not want to raise raise ContentSecurityPolicyConfigError, "`#csp_report_only=` was supplied a config with report_only: false. Use #csp=" else ContentSecurityPolicyReportOnlyConfig.new(new_csp) end end end if !@csp_report_only.opt_out? && @csp.to_h == ContentSecurityPolicyConfig::DEFAULT Kernel.warn "#{Kernel.caller.first}: [DEPRECATION] `#csp_report_only=` was configured before `#csp=`. It is assumed you intended to opt out of `#csp=` so be sure to add `config.csp = SecureHeaders::OPT_OUT` to your config. Ensure that #csp_report_only is configured after #csp=" @csp = OPT_OUT end end protected def cookies=(cookies) @cookies = cookies end def cached_headers=(headers) @cached_headers = headers end def hpkp=(hpkp) @hpkp = self.class.send(:deep_copy_if_hash, hpkp) end def hpkp_report_host=(hpkp_report_host) @hpkp_report_host = hpkp_report_host end private def cache_hpkp_report_host has_report_uri = @hpkp && @hpkp != OPT_OUT && @hpkp[:report_uri] self.hpkp_report_host = if has_report_uri parsed_report_uri = URI.parse(@hpkp[:report_uri]) parsed_report_uri.host end end # Public: Precompute the header names and values for this configuration. # Ensures that headers generated at configure time, not on demand. # # Returns the cached headers def cache_headers! # generate defaults for the "easy" headers headers = (ALL_HEADERS_BESIDES_CSP).each_with_object({}) do |klass, hash| config = instance_variable_get("@#{klass::CONFIG_KEY}") unless config == OPT_OUT hash[klass::CONFIG_KEY] = klass.make_header(config).freeze end end generate_csp_headers(headers) headers.freeze self.cached_headers = headers end # Private: adds CSP headers for each variation of CSP support. # # headers - generated headers are added to this hash namespaced by The # different variations # # Returns nothing def generate_csp_headers(headers) generate_csp_headers_for_config(headers, ContentSecurityPolicyConfig::CONFIG_KEY, self.csp) generate_csp_headers_for_config(headers, ContentSecurityPolicyReportOnlyConfig::CONFIG_KEY, self.csp_report_only) end def generate_csp_headers_for_config(headers, header_key, csp_config) unless csp_config.opt_out? headers[header_key] = {} ContentSecurityPolicy::VARIATIONS.each do |name, _| csp = ContentSecurityPolicy.make_header(csp_config, UserAgent.parse(name)) headers[header_key][name] = csp.freeze end end end end end secure_headers-3.7.1/lib/secure_headers/middleware.rb0000644000004100000410000000340313156767433022743 0ustar www-datawww-datamodule SecureHeaders class Middleware HPKP_SAME_HOST_WARNING = "[WARNING] HPKP report host should not be the same as the request host. See https://github.com/twitter/secureheaders/issues/166" def initialize(app) @app = app end # merges the hash of headers into the current header set. def call(env) req = Rack::Request.new(env) status, headers, response = @app.call(env) config = SecureHeaders.config_for(req) if config.hpkp_report_host == req.host Kernel.warn(HPKP_SAME_HOST_WARNING) end flag_cookies!(headers, override_secure(env, config.cookies)) if config.cookies headers.merge!(SecureHeaders.header_hash_for(req)) [status, headers, response] end private # inspired by https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L183-L194 def flag_cookies!(headers, config) if cookies = headers['Set-Cookie'] # Support Rails 2.3 / Rack 1.1 arrays as headers cookies = cookies.split("\n") unless cookies.is_a?(Array) headers['Set-Cookie'] = cookies.map do |cookie| SecureHeaders::Cookie.new(cookie, config).to_s end.join("\n") end end # disable Secure cookies for non-https requests def override_secure(env, config = {}) if scheme(env) != 'https' config.merge!(secure: false) end config end # derived from https://github.com/tobmatth/rack-ssl-enforcer/blob/6c014/lib/rack/ssl-enforcer.rb#L119 def scheme(env) if env['HTTPS'] == 'on' || env['HTTP_X_SSL_REQUEST'] == 'on' 'https' elsif env['HTTP_X_FORWARDED_PROTO'] env['HTTP_X_FORWARDED_PROTO'].split(',')[0] else env['rack.url_scheme'] end end end end secure_headers-3.7.1/lib/secure_headers/headers/0000755000004100000410000000000013156767433021714 5ustar www-datawww-datasecure_headers-3.7.1/lib/secure_headers/headers/x_xss_protection.rb0000644000004100000410000000164013156767433025654 0ustar www-datawww-datamodule SecureHeaders class XXssProtectionConfigError < StandardError; end class XXssProtection HEADER_NAME = 'X-XSS-Protection'.freeze DEFAULT_VALUE = "1; mode=block" VALID_X_XSS_HEADER = /\A[01](; mode=block)?(; report=.*)?\z/i CONFIG_KEY = :x_xss_protection class << self # Public: generate an X-Xss-Protection header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) raise XXssProtectionConfigError.new("Invalid format (see VALID_X_XSS_HEADER)") unless config.to_s =~ VALID_X_XSS_HEADER end end end end secure_headers-3.7.1/lib/secure_headers/headers/content_security_policy_config.rb0000644000004100000410000000740413156767433030553 0ustar www-datawww-datamodule SecureHeaders module DynamicConfig def self.included(base) base.send(:attr_writer, :modified) base.send(:attr_reader, *base.attrs) base.attrs.each do |attr| base.send(:define_method, "#{attr}=") do |value| if self.class.attrs.include?(attr) write_attribute(attr, value) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{attr}=#{value}" end end end end def initialize(hash) @base_uri = nil @block_all_mixed_content = nil @child_src = nil @connect_src = nil @default_src = nil @font_src = nil @form_action = nil @frame_ancestors = nil @frame_src = nil @img_src = nil @manifest_src = nil @media_src = nil @object_src = nil @plugin_types = nil @preserve_schemes = nil @report_only = nil @report_uri = nil @sandbox = nil @script_nonce = nil @script_src = nil @style_nonce = nil @style_src = nil @upgrade_insecure_requests = nil from_hash(hash) @modified = false end def update_directive(directive, value) self.send("#{directive}=", value) end def directive_value(directive) if self.class.attrs.include?(directive) self.send(directive) end end def modified? @modified end def merge(new_hash) ContentSecurityPolicy.combine_policies(self.to_h, new_hash) end def merge!(new_hash) from_hash(new_hash) end def append(new_hash) from_hash(ContentSecurityPolicy.combine_policies(self.to_h, new_hash)) end def to_h self.class.attrs.each_with_object({}) do |key, hash| value = self.send(key) hash[key] = value unless value.nil? end end def dup self.class.new(self.to_h) end def opt_out? false end def ==(o) self.class == o.class && self.to_h == o.to_h end alias_method :[], :directive_value alias_method :[]=, :update_directive private def from_hash(hash) hash.each_pair do |k, v| next if v.nil? if self.class.attrs.include?(k) write_attribute(k, v) else raise ContentSecurityPolicyConfigError, "Unknown config directive: #{k}=#{v}" end end end def write_attribute(attr, value) value = value.dup if PolicyManagement::DIRECTIVE_VALUE_TYPES[attr] == :source_list attr_variable = "@#{attr}" prev_value = self.instance_variable_get(attr_variable) self.instance_variable_set(attr_variable, value) if prev_value != value @modified = true end end end class ContentSecurityPolicyConfigError < StandardError; end class ContentSecurityPolicyConfig CONFIG_KEY = :csp HEADER_NAME = "Content-Security-Policy".freeze ATTRS = PolicyManagement::ALL_DIRECTIVES + PolicyManagement::META_CONFIGS + PolicyManagement::NONCES def self.attrs ATTRS end include DynamicConfig # based on what was suggested in https://github.com/rails/rails/pull/24961/files DEFAULT = { default_src: %w('self' https:), font_src: %w('self' https: data:), img_src: %w('self' https: data:), object_src: %w('none'), script_src: %w(https:), style_src: %w('self' https: 'unsafe-inline') } def report_only? false end def make_report_only ContentSecurityPolicyReportOnlyConfig.new(self.to_h) end end class ContentSecurityPolicyReportOnlyConfig < ContentSecurityPolicyConfig CONFIG_KEY = :csp_report_only HEADER_NAME = "Content-Security-Policy-Report-Only".freeze def report_only? true end def make_report_only self end end end secure_headers-3.7.1/lib/secure_headers/headers/public_key_pins.rb0000644000004100000410000000436013156767433025423 0ustar www-datawww-datamodule SecureHeaders class PublicKeyPinsConfigError < StandardError; end class PublicKeyPins HEADER_NAME = "Public-Key-Pins".freeze REPORT_ONLY = "Public-Key-Pins-Report-Only".freeze HASH_ALGORITHMS = [:sha256].freeze CONFIG_KEY = :hpkp class << self # Public: make an hpkp header name, value pair # # 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 PublicKeyPinsConfigError.new("config must be a hash.") unless config.is_a? Hash if !config[:max_age] raise PublicKeyPinsConfigError.new("max-age is a required directive.") elsif config[:max_age].to_s !~ /\A\d+\z/ raise PublicKeyPinsConfigError.new("max-age must be a number. #{config[:max_age]} was supplied.") elsif config[:pins] && config[:pins].length < 2 raise PublicKeyPinsConfigError.new("A minimum of 2 pins are required.") end end end def initialize(config) @max_age = config.fetch(:max_age, nil) @pins = config.fetch(:pins, nil) @report_uri = config.fetch(:report_uri, nil) @report_only = !!config.fetch(:report_only, nil) @include_subdomains = !!config.fetch(:include_subdomains, nil) end def name if @report_only REPORT_ONLY else HEADER_NAME end end def value [ max_age_directive, pin_directives, report_uri_directive, subdomain_directive ].compact.join('; ').strip end def pin_directives return nil if @pins.nil? @pins.collect do |pin| pin.map do |token, hash| "pin-#{token}=\"#{hash}\"" if HASH_ALGORITHMS.include?(token) end end.join('; ') 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 def subdomain_directive @include_subdomains ? 'includeSubDomains' : nil end end end secure_headers-3.7.1/lib/secure_headers/headers/x_permitted_cross_domain_policies.rb0000644000004100000410000000177613156767433031227 0ustar www-datawww-datamodule SecureHeaders class XPCDPConfigError < StandardError; end class XPermittedCrossDomainPolicies HEADER_NAME = "X-Permitted-Cross-Domain-Policies".freeze DEFAULT_VALUE = 'none' VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename) CONFIG_KEY = :x_permitted_cross_domain_policies class << self # Public: generate an X-Permitted-Cross-Domain-Policies header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless VALID_POLICIES.include?(config.downcase) raise XPCDPConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") end end end end end secure_headers-3.7.1/lib/secure_headers/headers/x_download_options.rb0000644000004100000410000000153213156767433026153 0ustar www-datawww-datamodule SecureHeaders class XDOConfigError < StandardError; end class XDownloadOptions HEADER_NAME = "X-Download-Options".freeze DEFAULT_VALUE = 'noopen' CONFIG_KEY = :x_download_options class << self # Public: generate an X-Download-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config.casecmp(DEFAULT_VALUE) == 0 raise XDOConfigError.new("Value can only be nil or 'noopen'") end end end end end secure_headers-3.7.1/lib/secure_headers/headers/policy_management.rb0000644000004100000410000003336213156767433025743 0ustar www-datawww-datamodule SecureHeaders module PolicyManagement def self.included(base) base.extend(ClassMethods) end MODERN_BROWSERS = %w(Chrome Opera Firefox) DEFAULT_VALUE = "default-src https:".freeze DEFAULT_CONFIG = { default_src: %w(https:) }.freeze DATA_PROTOCOL = "data:".freeze BLOB_PROTOCOL = "blob:".freeze SELF = "'self'".freeze NONE = "'none'".freeze STAR = "*".freeze UNSAFE_INLINE = "'unsafe-inline'".freeze UNSAFE_EVAL = "'unsafe-eval'".freeze STRICT_DYNAMIC = "'strict-dynamic'".freeze # leftover deprecated values that will be in common use upon upgrading. DEPRECATED_SOURCE_VALUES = [SELF, NONE, UNSAFE_EVAL, UNSAFE_INLINE, "inline", "eval"].map { |value| value.delete("'") }.freeze DEFAULT_SRC = :default_src CONNECT_SRC = :connect_src FONT_SRC = :font_src FRAME_SRC = :frame_src IMG_SRC = :img_src MEDIA_SRC = :media_src OBJECT_SRC = :object_src SANDBOX = :sandbox SCRIPT_SRC = :script_src STYLE_SRC = :style_src REPORT_URI = :report_uri DIRECTIVES_1_0 = [ DEFAULT_SRC, CONNECT_SRC, FONT_SRC, FRAME_SRC, IMG_SRC, MEDIA_SRC, OBJECT_SRC, SANDBOX, SCRIPT_SRC, STYLE_SRC, REPORT_URI ].freeze BASE_URI = :base_uri CHILD_SRC = :child_src FORM_ACTION = :form_action FRAME_ANCESTORS = :frame_ancestors PLUGIN_TYPES = :plugin_types DIRECTIVES_2_0 = [ DIRECTIVES_1_0, BASE_URI, CHILD_SRC, FORM_ACTION, FRAME_ANCESTORS, PLUGIN_TYPES ].flatten.freeze # All the directives currently under consideration for CSP level 3. # https://w3c.github.io/webappsec/specs/CSP2/ BLOCK_ALL_MIXED_CONTENT = :block_all_mixed_content MANIFEST_SRC = :manifest_src UPGRADE_INSECURE_REQUESTS = :upgrade_insecure_requests DIRECTIVES_3_0 = [ DIRECTIVES_2_0, BLOCK_ALL_MIXED_CONTENT, MANIFEST_SRC, UPGRADE_INSECURE_REQUESTS ].flatten.freeze EDGE_DIRECTIVES = DIRECTIVES_1_0 SAFARI_DIRECTIVES = DIRECTIVES_1_0 SAFARI_10_DIRECTIVES = DIRECTIVES_2_0 FIREFOX_UNSUPPORTED_DIRECTIVES = [ BLOCK_ALL_MIXED_CONTENT, CHILD_SRC, PLUGIN_TYPES ].freeze FIREFOX_46_DEPRECATED_DIRECTIVES = [ FRAME_SRC ].freeze FIREFOX_46_UNSUPPORTED_DIRECTIVES = [ BLOCK_ALL_MIXED_CONTENT, PLUGIN_TYPES ].freeze FIREFOX_DIRECTIVES = ( DIRECTIVES_3_0 - FIREFOX_UNSUPPORTED_DIRECTIVES ).freeze FIREFOX_46_DIRECTIVES = ( DIRECTIVES_3_0 - FIREFOX_46_UNSUPPORTED_DIRECTIVES - FIREFOX_46_DEPRECATED_DIRECTIVES ).freeze CHROME_DIRECTIVES = ( DIRECTIVES_3_0 ).freeze ALL_DIRECTIVES = (DIRECTIVES_1_0 + DIRECTIVES_2_0 + DIRECTIVES_3_0).uniq.sort # Think of default-src and report-uri as the beginning and end respectively, # everything else is in between. BODY_DIRECTIVES = ALL_DIRECTIVES - [DEFAULT_SRC, REPORT_URI] VARIATIONS = { "Chrome" => CHROME_DIRECTIVES, "Opera" => CHROME_DIRECTIVES, "Firefox" => FIREFOX_DIRECTIVES, "FirefoxTransitional" => FIREFOX_46_DIRECTIVES, "Safari" => SAFARI_DIRECTIVES, "SafariTransitional" => SAFARI_10_DIRECTIVES, "Edge" => EDGE_DIRECTIVES, "Other" => CHROME_DIRECTIVES }.freeze OTHER = "Other".freeze DIRECTIVE_VALUE_TYPES = { BASE_URI => :source_list, BLOCK_ALL_MIXED_CONTENT => :boolean, CHILD_SRC => :source_list, CONNECT_SRC => :source_list, DEFAULT_SRC => :source_list, FONT_SRC => :source_list, FORM_ACTION => :source_list, FRAME_ANCESTORS => :source_list, FRAME_SRC => :source_list, IMG_SRC => :source_list, MANIFEST_SRC => :source_list, MEDIA_SRC => :source_list, OBJECT_SRC => :source_list, PLUGIN_TYPES => :media_type_list, REPORT_URI => :source_list, SANDBOX => :sandbox_list, SCRIPT_SRC => :source_list, STYLE_SRC => :source_list, UPGRADE_INSECURE_REQUESTS => :boolean }.freeze # These are directives that don't have use a source list, and hence do not # inherit the default-src value. NON_SOURCE_LIST_SOURCES = DIRECTIVE_VALUE_TYPES.select do |_, type| type != :source_list end.keys.freeze # These are directives that take a source list, but that do not inherit # the default-src value. NON_FETCH_SOURCES = [ BASE_URI, FORM_ACTION, FRAME_ANCESTORS, REPORT_URI ] FETCH_SOURCES = ALL_DIRECTIVES - NON_FETCH_SOURCES - NON_SOURCE_LIST_SOURCES STAR_REGEXP = Regexp.new(Regexp.escape(STAR)) HTTP_SCHEME_REGEX = %r{\Ahttps?://} WILDCARD_SOURCES = [ UNSAFE_EVAL, UNSAFE_INLINE, STAR, DATA_PROTOCOL, BLOB_PROTOCOL ].freeze META_CONFIGS = [ :report_only, :preserve_schemes ].freeze NONCES = [ :script_nonce, :style_nonce ].freeze module ClassMethods # Public: generate a header name, value array that is user-agent-aware. # # Returns a default policy if no configuration is provided, or a # header name and value based on the config. def make_header(config, user_agent) header = new(config, user_agent) [header.name, header.value] end # Public: Validates each source expression. # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_config!(config) return if config.nil? || config.opt_out? raise ContentSecurityPolicyConfigError.new(":default_src is required") unless config.directive_value(:default_src) ContentSecurityPolicyConfig.attrs.each do |key| value = config.directive_value(key) next unless value if META_CONFIGS.include?(key) raise ContentSecurityPolicyConfigError.new("#{key} must be a boolean value") unless boolean?(value) || value.nil? else validate_directive!(key, value) end end end # Public: check if a user agent supports CSP nonces # # user_agent - a String or a UserAgent object def nonces_supported?(user_agent) user_agent = UserAgent.parse(user_agent) if user_agent.is_a?(String) MODERN_BROWSERS.include?(user_agent.browser) || user_agent.browser == "Safari" && (user_agent.version || CSP::FALLBACK_VERSION) >= CSP::VERSION_10 end # Public: combine the values from two different configs. # # original - the main config # additions - values to be merged in # # raises an error if the original config is OPT_OUT # # 1. for non-source-list values (report_only, block_all_mixed_content, upgrade_insecure_requests), # additions will overwrite the original value. # 2. if a value in additions does not exist in the original config, the # default-src value is included to match original behavior. # 3. if a value in additions does exist in the original config, the two # values are joined. def combine_policies(original, additions) if original == {} raise ContentSecurityPolicyConfigError.new("Attempted to override an opt-out CSP config.") end original = Configuration.send(:deep_copy, original) populate_fetch_source_with_default!(original, additions) merge_policy_additions(original, additions) end def ua_to_variation(user_agent) family = user_agent.browser if family && VARIATIONS.key?(family) family else OTHER end end private # merge the two hashes. combine (instead of overwrite) the array values # when each hash contains a value for a given key. def merge_policy_additions(original, additions) original.merge(additions) do |directive, lhs, rhs| if list_directive?(directive) (lhs.to_a + rhs.to_a).compact.uniq else rhs end end.reject { |_, value| value.nil? || value == [] } # this mess prevents us from adding empty directives. end # Returns True if a directive expects a list of values and False otherwise. def list_directive?(directive) source_list?(directive) || sandbox_list?(directive) || media_type_list?(directive) end # For each directive in additions that does not exist in the original config, # copy the default-src value to the original config. This modifies the original hash. def populate_fetch_source_with_default!(original, additions) # in case we would be appending to an empty directive, fill it with the default-src value additions.each_key do |directive| directive = if directive.to_s.end_with?("_nonce") directive.to_s.gsub(/_nonce/, "_src").to_sym else directive end # Don't set a default if directive has an existing value next if original[directive] if FETCH_SOURCES.include?(directive) original[directive] = default_for(directive, original) end end end def default_for(directive, original) return original[FRAME_SRC] if directive == CHILD_SRC && original[FRAME_SRC] return original[CHILD_SRC] if directive == FRAME_SRC && original[CHILD_SRC] original[DEFAULT_SRC] end def source_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :source_list end def sandbox_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :sandbox_list end def media_type_list?(directive) DIRECTIVE_VALUE_TYPES[directive] == :media_type_list end # Private: Validates that the configuration has a valid type, or that it is a valid # source expression. def validate_directive!(directive, value) ensure_valid_directive!(directive) case ContentSecurityPolicy::DIRECTIVE_VALUE_TYPES[directive] when :boolean unless boolean?(value) raise ContentSecurityPolicyConfigError.new("#{directive} must be a boolean. Found #{value.class} value") end when :sandbox_list validate_sandbox_expression!(directive, value) when :media_type_list validate_media_type_expression!(directive, value) when :source_list validate_source_expression!(directive, value) else raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end end # Private: validates that a sandbox token expression: # 1. is an array of strings or optionally `true` (to enable maximal sandboxing) # 2. For arrays, each element is of the form allow-* def validate_sandbox_expression!(directive, sandbox_token_expression) # We support sandbox: true to indicate a maximally secure sandbox. return if boolean?(sandbox_token_expression) && sandbox_token_expression == true ensure_array_of_strings!(directive, sandbox_token_expression) valid = sandbox_token_expression.compact.all? do |v| v.is_a?(String) && v.start_with?("allow-") end if !valid raise ContentSecurityPolicyConfigError.new("#{directive} must be True or an array of zero or more sandbox token strings (ex. allow-forms)") end end # Private: validates that a media type expression: # 1. is an array of strings # 2. each element is of the form type/subtype def validate_media_type_expression!(directive, media_type_expression) ensure_array_of_strings!(directive, media_type_expression) valid = media_type_expression.compact.all? do |v| # All media types are of the form: "/" . v =~ /\A.+\/.+\z/ end if !valid raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of valid media types (ex. application/pdf)") end end # Private: validates that a source expression: # 1. is an array of strings # 2. does not contain any deprecated, now invalid values (inline, eval, self, none) # # Does not validate the invididual values of the source expression (e.g. # script_src => h*t*t*p: will not raise an exception) def validate_source_expression!(directive, source_expression) if source_expression != OPT_OUT ensure_array_of_strings!(directive, source_expression) end ensure_valid_sources!(directive, source_expression) end def ensure_valid_directive!(directive) unless ContentSecurityPolicy::ALL_DIRECTIVES.include?(directive) raise ContentSecurityPolicyConfigError.new("Unknown directive #{directive}") end end def ensure_array_of_strings!(directive, value) if (!value.is_a?(Array) || !value.compact.all? { |v| v.is_a?(String) }) raise ContentSecurityPolicyConfigError.new("#{directive} must be an array of strings") end end def ensure_valid_sources!(directive, source_expression) source_expression.each do |expression| if ContentSecurityPolicy::DEPRECATED_SOURCE_VALUES.include?(expression) raise ContentSecurityPolicyConfigError.new("#{directive} contains an invalid keyword source (#{expression}). This value must be single quoted.") end end end def boolean?(source_expression) source_expression.is_a?(TrueClass) || source_expression.is_a?(FalseClass) end end end end secure_headers-3.7.1/lib/secure_headers/headers/clear_site_data.rb0000644000004100000410000000315313156767433025346 0ustar www-datawww-datamodule SecureHeaders class ClearSiteDataConfigError < StandardError; end class ClearSiteData HEADER_NAME = "Clear-Site-Data".freeze # Valid `types` CACHE = "cache".freeze COOKIES = "cookies".freeze STORAGE = "storage".freeze EXECTION_CONTEXTS = "executionContexts".freeze ALL_TYPES = [CACHE, COOKIES, STORAGE, EXECTION_CONTEXTS] CONFIG_KEY = :clear_site_data class << self # Public: make an Clear-Site-Data header name, value pair # # Returns nil if not configured, returns header name and value if configured. def make_header(config=nil) case config when nil, OPT_OUT, [] # noop when Array [HEADER_NAME, make_header_value(config)] when true [HEADER_NAME, make_header_value(ALL_TYPES)] end end def validate_config!(config) case config when nil, OPT_OUT, true # valid when Array unless config.all? { |t| t.is_a?(String) } raise ClearSiteDataConfigError.new("types must be Strings") end else raise ClearSiteDataConfigError.new("config must be an Array of Strings or `true`") end end # Public: Transform a Clear-Site-Data config (an Array of Strings) into a # String that can be used as the value for the Clear-Site-Data header. # # types - An Array of String of types of data to clear. # # Returns a String of quoted values that are comma separated. def make_header_value(types) types.map { |t| "\"#{t}\""}.join(", ") end end end end secure_headers-3.7.1/lib/secure_headers/headers/strict_transport_security.rb0000644000004100000410000000203213156767433027611 0ustar www-datawww-datamodule SecureHeaders class STSConfigError < StandardError; end class StrictTransportSecurity HEADER_NAME = 'Strict-Transport-Security'.freeze HSTS_MAX_AGE = "631138519" DEFAULT_VALUE = "max-age=" + HSTS_MAX_AGE VALID_STS_HEADER = /\Amax-age=\d+(; includeSubdomains)?(; preload)?\z/i MESSAGE = "The config value supplied for the HSTS header was invalid. Must match #{VALID_STS_HEADER}" CONFIG_KEY = :hsts class << self # Public: generate an hsts header name, value pair. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config} #{config.class}") unless config.is_a?(String) raise STSConfigError.new(MESSAGE) unless config =~ VALID_STS_HEADER end end end end secure_headers-3.7.1/lib/secure_headers/headers/expect_certificate_transparency.rb0000644000004100000410000000430213156767433030663 0ustar www-datawww-data# 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 secure_headers-3.7.1/lib/secure_headers/headers/referrer_policy.rb0000644000004100000410000000215413156767433025436 0ustar www-datawww-datamodule SecureHeaders class ReferrerPolicyConfigError < StandardError; end class ReferrerPolicy HEADER_NAME = "Referrer-Policy".freeze DEFAULT_VALUE = "origin-when-cross-origin" VALID_POLICIES = %w( no-referrer no-referrer-when-downgrade same-origin strict-origin strict-origin-when-cross-origin origin origin-when-cross-origin unsafe-url ) CONFIG_KEY = :referrer_policy class << self # Public: generate an Referrer Policy header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless VALID_POLICIES.include?(config.downcase) raise ReferrerPolicyConfigError.new("Value can only be one of #{VALID_POLICIES.join(', ')}") end end end end end secure_headers-3.7.1/lib/secure_headers/headers/x_frame_options.rb0000644000004100000410000000210013156767433025426 0ustar www-datawww-datamodule SecureHeaders class XFOConfigError < StandardError; end class XFrameOptions HEADER_NAME = "X-Frame-Options".freeze CONFIG_KEY = :x_frame_options SAMEORIGIN = "sameorigin" DENY = "deny" ALLOW_FROM = "allow-from" ALLOW_ALL = "allowall" DEFAULT_VALUE = SAMEORIGIN VALID_XFO_HEADER = /\A(#{SAMEORIGIN}\z|#{DENY}\z|#{ALLOW_ALL}\z|#{ALLOW_FROM}[:\s])/i class << self # Public: generate an X-Frame-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) return if config == OPT_OUT [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config =~ VALID_XFO_HEADER raise XFOConfigError.new("Value must be SAMEORIGIN|DENY|ALLOW-FROM:|ALLOWALL") end end end end end secure_headers-3.7.1/lib/secure_headers/headers/content_security_policy.rb0000644000004100000410000002101513156767433027220 0ustar www-datawww-datarequire_relative 'policy_management' require_relative 'content_security_policy_config' require 'useragent' module SecureHeaders class ContentSecurityPolicy include PolicyManagement # constants to be used for version-specific UA sniffing VERSION_46 = ::UserAgent::Version.new("46") VERSION_10 = ::UserAgent::Version.new("10") FALLBACK_VERSION = ::UserAgent::Version.new("0") def initialize(config = nil, user_agent = OTHER) @config = if config.is_a?(Hash) if config[:report_only] ContentSecurityPolicyReportOnlyConfig.new(config || DEFAULT_CONFIG) else ContentSecurityPolicyConfig.new(config || DEFAULT_CONFIG) end elsif config.nil? ContentSecurityPolicyConfig.new(DEFAULT_CONFIG) else config end @parsed_ua = if user_agent.is_a?(UserAgent::Browsers::Base) user_agent else UserAgent.parse(user_agent) end @frame_src = normalize_child_frame_src @preserve_schemes = @config.preserve_schemes @script_nonce = @config.script_nonce @style_nonce = @config.style_nonce end ## # Returns the name to use for the header. Either "Content-Security-Policy" or # "Content-Security-Policy-Report-Only" def name @config.class.const_get(:HEADER_NAME) end ## # Return the value of the CSP header def value @value ||= if @config build_value else DEFAULT_VALUE end end private # frame-src is deprecated, child-src is being implemented. They are # very similar and in most cases, the same value can be used for both. def normalize_child_frame_src if @config.frame_src && @config.child_src && @config.frame_src != @config.child_src Kernel.warn("#{Kernel.caller.first}: [DEPRECATION] both :child_src and :frame_src supplied and do not match. This can lead to inconsistent behavior across browsers.") end if supported_directives.include?(:child_src) @config.child_src || @config.frame_src else @config.frame_src || @config.child_src end end # Private: converts the config object into a string representing a policy. # Places default-src at the first directive and report-uri as the last. All # others are presented in alphabetical order. # # Unsupported directives are filtered based on the user agent. # # Returns a content security policy header value. def build_value directives.map do |directive_name| case DIRECTIVE_VALUE_TYPES[directive_name] when :boolean symbol_to_hyphen_case(directive_name) if @config.directive_value(directive_name) when :sandbox_list build_sandbox_list_directive(directive_name) when :media_type_list build_media_type_list_directive(directive_name) when :source_list build_source_list_directive(directive_name) end end.compact.join("; ") end def build_sandbox_list_directive(directive) return unless sandbox_list = @config.directive_value(directive) max_strict_policy = case sandbox_list when Array sandbox_list.empty? when true true else false end # A maximally strict sandbox policy is just the `sandbox` directive, # whith no configuraiton values. if max_strict_policy symbol_to_hyphen_case(directive) elsif sandbox_list && sandbox_list.any? [ symbol_to_hyphen_case(directive), sandbox_list.uniq ].join(" ") end end def build_media_type_list_directive(directive) return unless media_type_list = @config.directive_value(directive) if media_type_list && media_type_list.any? [ symbol_to_hyphen_case(directive), media_type_list.uniq ].join(" ") end end # Private: builds a string that represents one directive in a minified form. # # directive_name - a symbol representing the various ALL_DIRECTIVES # # Returns a string representing a directive. def build_source_list_directive(directive) source_list = case directive when :child_src if supported_directives.include?(:child_src) @frame_src end when :frame_src unless supported_directives.include?(:child_src) @frame_src end else @config.directive_value(directive) end return unless source_list && source_list.any? normalized_source_list = minify_source_list(directive, source_list) [symbol_to_hyphen_case(directive), normalized_source_list].join(" ") end # If a directive contains *, all other values are omitted. # If a directive contains 'none' but has other values, 'none' is ommitted. # Schemes are stripped (see http://www.w3.org/TR/CSP2/#match-source-expression) def minify_source_list(directive, source_list) source_list = source_list.compact if source_list.include?(STAR) keep_wildcard_sources(source_list) else source_list = populate_nonces(directive, source_list) source_list = reject_all_values_if_none(source_list) unless directive == REPORT_URI || @preserve_schemes source_list = strip_source_schemes(source_list) end dedup_source_list(source_list) end end # Discard trailing entries (excluding unsafe-*) since * accomplishes the same. def keep_wildcard_sources(source_list) source_list.select { |value| WILDCARD_SOURCES.include?(value) } end # Discard any 'none' values if more directives are supplied since none may override values. def reject_all_values_if_none(source_list) if source_list.length > 1 source_list.reject { |value| value == NONE } else source_list end end # Removes duplicates and sources that already match an existing wild card. # # e.g. *.github.com asdf.github.com becomes *.github.com def dedup_source_list(sources) sources = sources.uniq wild_sources = sources.select { |source| source =~ STAR_REGEXP } if wild_sources.any? sources.reject do |source| !wild_sources.include?(source) && wild_sources.any? { |pattern| File.fnmatch(pattern, source) } end else sources end end # Private: append a nonce to the script/style directories if script_nonce # or style_nonce are provided. def populate_nonces(directive, source_list) case directive when SCRIPT_SRC append_nonce(source_list, @script_nonce) when STYLE_SRC append_nonce(source_list, @style_nonce) else source_list end end # Private: adds a nonce or 'unsafe-inline' depending on browser support. # If a nonce is populated, inline content is assumed. # # While CSP is backward compatible in that a policy with a nonce will ignore # unsafe-inline, this is more concise. def append_nonce(source_list, nonce) if nonce if nonces_supported? source_list << "'nonce-#{nonce}'" else source_list << UNSAFE_INLINE end end source_list end # Private: return the list of directives that are supported by the user agent, # starting with default-src and ending with report-uri. def directives [ DEFAULT_SRC, BODY_DIRECTIVES.select { |key| supported_directives.include?(key) }, REPORT_URI ].flatten end # Private: Remove scheme from source expressions. def strip_source_schemes(source_list) source_list.map { |source_expression| source_expression.sub(HTTP_SCHEME_REGEX, "") } end # Private: determine which directives are supported for the given user agent. # # Add UA-sniffing special casing here. # # Returns an array of symbols representing the directives. def supported_directives @supported_directives ||= if VARIATIONS[@parsed_ua.browser] if @parsed_ua.browser == "Firefox" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_46) VARIATIONS["FirefoxTransitional"] elsif @parsed_ua.browser == "Safari" && ((@parsed_ua.version || FALLBACK_VERSION) >= VERSION_10) VARIATIONS["SafariTransitional"] else VARIATIONS[@parsed_ua.browser] end else VARIATIONS[OTHER] end end def nonces_supported? @nonces_supported ||= self.class.nonces_supported?(@parsed_ua) end def symbol_to_hyphen_case(sym) sym.to_s.tr('_', '-') end end end secure_headers-3.7.1/lib/secure_headers/headers/cookie.rb0000644000004100000410000000513513156767433023516 0ustar www-datawww-datarequire 'cgi' require 'secure_headers/utils/cookies_config' module SecureHeaders class CookiesConfigError < StandardError; end class Cookie class << self def validate_config!(config) CookiesConfig.new(config).validate! end end attr_reader :raw_cookie, :config def initialize(cookie, config) @raw_cookie = cookie @config = config @attributes = { httponly: nil, samesite: nil, secure: nil, } parse(cookie) end def to_s @raw_cookie.dup.tap do |c| c << "; secure" if secure? c << "; HttpOnly" if httponly? c << "; #{samesite_cookie}" if samesite? end end def secure? flag_cookie?(:secure) && !already_flagged?(:secure) end def httponly? flag_cookie?(:httponly) && !already_flagged?(:httponly) end def samesite? flag_samesite? && !already_flagged?(:samesite) end private def parsed_cookie @parsed_cookie ||= CGI::Cookie.parse(raw_cookie) end def already_flagged?(attribute) @attributes[attribute] end def flag_cookie?(attribute) case config[attribute] when TrueClass true when Hash conditionally_flag?(config[attribute]) else false end end def conditionally_flag?(configuration) if(Array(configuration[:only]).any? && (Array(configuration[:only]) & parsed_cookie.keys).any?) true elsif(Array(configuration[:except]).any? && (Array(configuration[:except]) & parsed_cookie.keys).none?) true else false end end def samesite_cookie if flag_samesite_lax? "SameSite=Lax" elsif flag_samesite_strict? "SameSite=Strict" end end def flag_samesite? flag_samesite_lax? || flag_samesite_strict? end def flag_samesite_lax? flag_samesite_enforcement?(:lax) end def flag_samesite_strict? flag_samesite_enforcement?(:strict) end def flag_samesite_enforcement?(mode) return unless config[:samesite] case config[:samesite][mode] when Hash conditionally_flag?(config[:samesite][mode]) when TrueClass true else false end end def parse(cookie) return unless cookie cookie.split(/[;,]\s?/).each do |pairs| name, values = pairs.split('=',2) name = CGI.unescape(name) attribute = name.downcase.to_sym if @attributes.has_key?(attribute) @attributes[attribute] = values || true end end end end end secure_headers-3.7.1/lib/secure_headers/headers/x_content_type_options.rb0000644000004100000410000000161413156767433027060 0ustar www-datawww-datamodule SecureHeaders class XContentTypeOptionsConfigError < StandardError; end class XContentTypeOptions HEADER_NAME = "X-Content-Type-Options".freeze DEFAULT_VALUE = "nosniff" CONFIG_KEY = :x_content_type_options class << self # Public: generate an X-Content-Type-Options header. # # Returns a default header if no configuration is provided, or a # header name and value based on the config. def make_header(config = nil) [HEADER_NAME, config || DEFAULT_VALUE] end def validate_config!(config) return if config.nil? || config == OPT_OUT raise TypeError.new("Must be a string. Found #{config.class}: #{config}") unless config.is_a?(String) unless config.casecmp(DEFAULT_VALUE) == 0 raise XContentTypeOptionsConfigError.new("Value can only be nil or 'nosniff'") end end end end end secure_headers-3.7.1/lib/secure_headers/utils/0000755000004100000410000000000013156767433021441 5ustar www-datawww-datasecure_headers-3.7.1/lib/secure_headers/utils/cookies_config.rb0000644000004100000410000000677413156767433024765 0ustar www-datawww-datamodule SecureHeaders class CookiesConfig attr_reader :config def initialize(config) @config = config end def validate! return if config.nil? || config == SecureHeaders::OPT_OUT validate_config! validate_secure_config! if config[:secure] validate_httponly_config! if config[:httponly] validate_samesite_config! if config[:samesite] end private def validate_config! raise CookiesConfigError.new("config must be a hash.") unless is_hash?(config) end def validate_secure_config! validate_hash_or_boolean!(:secure) validate_exclusive_use_of_hash_constraints!(config[:secure], :secure) end def validate_httponly_config! validate_hash_or_boolean!(:httponly) validate_exclusive_use_of_hash_constraints!(config[:httponly], :httponly) end def validate_samesite_config! raise CookiesConfigError.new("samesite cookie config must be a hash") unless is_hash?(config[:samesite]) validate_samesite_boolean_config! validate_samesite_hash_config! end # when configuring with booleans, only one enforcement is permitted def validate_samesite_boolean_config! if config[:samesite].key?(:lax) && config[:samesite][:lax].is_a?(TrueClass) && config[:samesite].key?(:strict) raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") elsif config[:samesite].key?(:strict) && config[:samesite][:strict].is_a?(TrueClass) && config[:samesite].key?(:lax) raise CookiesConfigError.new("samesite cookie config is invalid, combination use of booleans and Hash to configure lax and strict enforcement is not permitted.") end end def validate_samesite_hash_config! # validate Hash-based samesite configuration if is_hash?(config[:samesite][:lax]) validate_exclusive_use_of_hash_constraints!(config[:samesite][:lax], 'samesite lax') if is_hash?(config[:samesite][:strict]) validate_exclusive_use_of_hash_constraints!(config[:samesite][:strict], 'samesite strict') validate_exclusive_use_of_samesite_enforcement!(:only) validate_exclusive_use_of_samesite_enforcement!(:except) end end end def validate_hash_or_boolean!(attribute) if !(is_hash?(config[attribute]) || is_boolean?(config[attribute])) raise CookiesConfigError.new("#{attribute} cookie config must be a hash or boolean") end end # validate exclusive use of only or except but not both at the same time def validate_exclusive_use_of_hash_constraints!(conf, attribute) return unless is_hash?(conf) if conf.key?(:only) && conf.key?(:except) raise CookiesConfigError.new("#{attribute} cookie config is invalid, simultaneous use of conditional arguments `only` and `except` is not permitted.") end end # validate exclusivity of only and except members within strict and lax def validate_exclusive_use_of_samesite_enforcement!(attribute) if (intersection = (config[:samesite][:lax].fetch(attribute, []) & config[:samesite][:strict].fetch(attribute, []))).any? raise CookiesConfigError.new("samesite cookie config is invalid, cookie(s) #{intersection.join(', ')} cannot be enforced as lax and strict") end end def is_hash?(obj) obj && obj.is_a?(Hash) end def is_boolean?(obj) obj && (obj.is_a?(TrueClass) || obj.is_a?(FalseClass)) end end end secure_headers-3.7.1/lib/secure_headers/railtie.rb0000644000004100000410000000260413156767433022261 0ustar www-datawww-data# rails 3.1+ if defined?(Rails::Railtie) module SecureHeaders class Railtie < Rails::Railtie isolate_namespace SecureHeaders if defined? isolate_namespace # rails 3.0 conflicting_headers = ['X-Frame-Options', 'X-XSS-Protection', 'X-Permitted-Cross-Domain-Policies', 'X-Download-Options', 'X-Content-Type-Options', 'Strict-Transport-Security', 'Content-Security-Policy', 'Content-Security-Policy-Report-Only', 'Public-Key-Pins', 'Public-Key-Pins-Report-Only', 'Referrer-Policy'] initializer "secure_headers.middleware" do Rails.application.config.middleware.insert_before 0, SecureHeaders::Middleware end rake_tasks do load File.expand_path(File.join('..', '..', 'lib', 'tasks', 'tasks.rake'), File.dirname(__FILE__)) end initializer "secure_headers.action_controller" do ActiveSupport.on_load(:action_controller) do include SecureHeaders unless Rails.application.config.action_dispatch.default_headers.nil? conflicting_headers.each do |header| Rails.application.config.action_dispatch.default_headers.delete(header) end end end end end end else module ActionController class Base include SecureHeaders end end end secure_headers-3.7.1/lib/secure_headers/hash_helper.rb0000644000004100000410000000044113156767433023107 0ustar www-datawww-datarequire 'base64' module SecureHeaders module HashHelper def hash_source(inline_script, digest = :SHA256) base64_hashed_content = Base64.encode64(Digest.const_get(digest).digest(inline_script)).chomp "'#{digest.to_s.downcase}-#{base64_hashed_content}'" end end end secure_headers-3.7.1/.gitignore0000644000004100000410000000015113156767433016537 0ustar www-datawww-data*.gem *.DS_STORE *.rbc .bundle .config .yardoc *.log Gemfile.lock _yardoc coverage pkg rdoc spec/reports secure_headers-3.7.1/upgrading-to-3-0.md0000644000004100000410000001641513156767433020000 0ustar www-datawww-data`secure_headers` 3.0 is a near-complete rewrite. It includes breaking changes and removes a lot of features that were either leftover from the days when the CSP standard was not fully adopted or were just downright confusing. Changes == | What | < = 2.x | >= 3.0 | | ---------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Global configuration | `SecureHeaders::Configuration.configure` block | `SecureHeaders::Configuration.default` block | | All headers besides HPKP and CSP | Accept hashes as config values | Must be strings (validated during configuration) | | CSP directive values | Accepted space delimited strings OR arrays of strings | Must be arrays of strings | | CSP Nonce values in views | `@content_security_policy_nonce` | `content_security_policy_nonce(:script)` or `content_security_policy_nonce(:style)` | | nonce is no longer a source expression | `config.csp = "'self' 'nonce'"` | Remove `'nonce'` from source expression and use [nonce helpers](https://github.com/twitter/secureheaders#nonce). | | `self`/`none` source expressions | Could be `self` / `none` / `'self'` / `'none'` | Must be `'self'` or `'none'` | | `inline` / `eval` source expressions | Could be `inline`, `eval`, `'unsafe-inline'`, or `'unsafe-eval'` | Must be `'unsafe-eval'` or `'unsafe-inline'` | | Per-action configuration | Override [`def secure_header_options_for(header, options)`](https://github.com/twitter/secureheaders/commit/bb9ebc6c12a677aad29af8e0f08ffd1def56efec#diff-04c6e90faac2675aa89e2176d2eec7d8R111) | Use [named overrides](https://github.com/twitter/secureheaders#named-overrides) or [per-action helpers](https://github.com/twitter/secureheaders#per-action-configuration) | | CSP/HPKP use `report_only` config that defaults to false | `enforce: false` | `report_only: false` | | Schemes in source expressions | Schemes were not stripped | Schemes are stripped by default to discourage mixed content. Setting `preserve_schemes: true` will revert to previous behavior | | Opting out of default configuration | `skip_before_filter :set_x_download_options_header` or `config.x_download_options = false` | Within default block: `config.x_download_options = SecureHeaders::OPT_OUT` | Migrating to 3.x from <= 2.x == 1. Convert all headers except for CSP/HPKP using hashes to string values. The values are validated at runtime and will provide guidance on misconfigured headers. 1. Convert all instances of `self`/`none`/`eval`/`inline` to the corresponding values in the above table. 1. Convert all CSP space-delimited directives to an array of strings. 1. Convert all `enforce: true|false` to `report_only: true|false`. 1. Remove `ensure_security_headers` from controllers (3.x uses a middleware instead). Everything is terrible, why should I upgrade? == `secure_headers` <= 2.x built every header per request using a series of automatically included `before_filters`. This is horribly inefficient because: 1. `before_filters` are slow and adding 8 per request isn't great 1. We are rebuilding strings that may never change for every request 1. Errors in the request may mean that the headers never get set in the first place `secure_headers` 3.x sets headers in rack middleware that runs once per request and uses configuration values passed via `request.env`. This is much more efficient and somewhat guarantees that headers will always be set. **The values for the headers are cached and reused per request**. Also, there is a more flexible API for customizing content security policies / X-Frame-Options. In practice, none of the other headers need granular controls. One way of customizing headers per request is to use the helper methods. The only downside of this technique is that headers will be computed from scratch. See the [README](README.md) for more information. secure_headers-3.7.1/docs/0000755000004100000410000000000013156767433015502 5ustar www-datawww-datasecure_headers-3.7.1/docs/hashes.md0000644000004100000410000000444413156767433017305 0ustar www-datawww-data## Hash `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. You can add hash sources directly to your policy : ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') # this is a made up value but browsers will show the expected hash in the console. script_src: %w(sha256-123456) } end ``` You can also use the automated inline script detection/collection/computation of hash source values in your app. ```bash rake secure_headers:generate_hashes ``` This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- scripts: app/views/asdfs/index.html.erb: - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" styles: app/views/asdfs/index.html.erb: - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` ##### Helpers **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. ```erb <%= hashed_style_tag do %> body { background-color: black; } <% end %> <%= hashed_style_tag do %> body { font-size: 30px; font-color: green; } <% end %> <%= hashed_javascript_tag do %> console.log(1) <% end %> ``` ``` Content-Security-Policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` secure_headers-3.7.1/docs/named_overrides_and_appends.md0000644000004100000410000000711613156767433023533 0ustar www-datawww-data## Named Appends Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. ```ruby def show if include_widget? @widget = widget.render use_content_security_policy_named_append(:widget_partial) end end SecureHeaders::Configuration.named_append(:widget_partial) do |request| SecureHeaders.override_x_frame_options(request, "DENY") if request.controller_instance.current_user.in_test_bucket? { child_src: %w(beta.thirdpartyhost.com) } else { child_src: %w(thirdpartyhost.com) } end end ``` You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: ```ruby SecureHeader::Configuration.default do |config| config.csp = { default_src: %w('self')} end SecureHeaders::Configuration.named_append(:A) do |request| { default_src: %w(myhost.com) } end SecureHeaders::Configuration.named_append(:B) do |request| { script_src: %w('unsafe-eval') } end ``` The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): ```ruby def index use_content_security_policy_named_append(:A) use_content_security_policy_named_append(:B) # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; end def show use_content_security_policy_named_append(:B) use_content_security_policy_named_append(:A) # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; end ``` ## Named overrides Named overrides serve two purposes: * To be able to refer to a configuration by simple name. * By precomputing the headers for a named configuration, the headers generated once and reused over every request. To use a named override, drop a `SecureHeaders::Configuration.override` block **outside** of method definitions and then declare which named override you'd like to use. You can even override an override. ```ruby class ApplicationController < ActionController::Base SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w(example.org) } end # override default configuration SecureHeaders::Configuration.override(:script_from_otherdomain_com) do |config| config.csp[:script_src] << "otherdomain.com" end # overrides the :script_from_otherdomain_com configuration SecureHeaders::Configuration.override(:another_config, :script_from_otherdomain_com) do |config| config.csp[:script_src] << "evenanotherdomain.com" end end class MyController < ApplicationController def index # Produces default-src 'self'; script-src example.org otherdomain.com use_secure_headers_override(:script_from_otherdomain_com) end def show # Produces default-src 'self'; script-src example.org otherdomain.org evenanotherdomain.com use_secure_headers_override(:another_config) end end ``` By default, a no-op configuration is provided. No headers will be set when this default override is used. ```ruby class MyController < ApplicationController def index SecureHeaders.opt_out_of_all_protection(request) end end ``` secure_headers-3.7.1/docs/HPKP.md0000644000004100000410000000136613156767433016574 0ustar www-datawww-data## HTTP Public Key Pins Be aware that pinning error reporting is governed by the same rules as everything else. If you have a pinning failure that tries to report back to the same origin, by definition this will not work. ```ruby config.hpkp = { max_age: 60.days.to_i, # max_age is a required parameter include_subdomains: true, # whether or not to apply pins to subdomains # Per the spec, SHA256 hashes are the only currently supported format. pins: [ {sha256: 'b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c'}, {sha256: '73a2c64f9545172c1195efb6616ca5f7afd1df6f245407cafb90de3998a1c97f'} ], report_only: true, # defaults to false (report-only mode) report_uri: 'https://report-uri.io/example-hpkp' } ``` secure_headers-3.7.1/docs/sinatra.md0000644000004100000410000000066013156767433017467 0ustar www-datawww-data## Sinatra Here's an example using SecureHeaders for Sinatra applications: ```ruby require 'rubygems' require 'sinatra' require 'haml' require 'secure_headers' use SecureHeaders::Middleware SecureHeaders::Configuration.default do |config| ... end class Donkey < Sinatra::Application set :root, APP_ROOT get '/' do SecureHeaders.override_x_frame_options(request, SecureHeaders::OPT_OUT) haml :index end end ``` secure_headers-3.7.1/docs/cookies.md0000644000004100000410000000245713156767433017470 0ustar www-datawww-data## Cookies SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. #### Boolean-based configuration Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. ```ruby config.cookies = { secure: true, # mark all cookies as Secure httponly: false, # do not mark any cookies as HttpOnly } ``` #### Hash-based configuration Hash-based configuration allows for fine-grained control. ```ruby config.cookies = { secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly } ``` #### SameSite cookie configuration SameSite cookies permit either `Strict` or `Lax` enforcement mode options. ```ruby config.cookies = { samesite: { strict: true # mark all cookies as SameSite=Strict } } ``` `Strict` and `Lax` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { samesite: { strict: { only: ['_rails_session'] }, lax: { only: ['_guest'] } } } ``` secure_headers-3.7.1/docs/per_action_configuration.md0000644000004100000410000000726013156767433023103 0ustar www-datawww-data## Per-action configuration You can override the settings for a given action by producing a temporary override. Be aware that because of the dynamic nature of the value, the header values will be computed per request. ```ruby # Given a config of: ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self'), script_src: %w('self') } end class MyController < ApplicationController def index # Append value to the source list, override 'none' values # Produces: default-src 'self'; script-src 'self' s3.amazonaws.com; object-src 'self' www.youtube.com append_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self' www.youtube.com)) # Overrides the previously set source list, override 'none' values # Produces: default-src 'self'; script-src s3.amazonaws.com; object-src 'self' override_content_security_policy_directives(script_src: %w(s3.amazonaws.com), object_src: %w('self')) # Global settings default to "sameorigin" override_x_frame_options("DENY") end ``` The following methods are available as controller instance methods. They are also available as class methods, but require you to pass in the `request` object. * `append_content_security_policy_directives(hash)`: appends each value to the corresponding CSP app-wide configuration. * `override_content_security_policy_directives(hash)`: merges the hash into the app-wide configuration, overwriting any previous config * `override_x_frame_options(value)`: sets the `X-Frame-Options header` to `value` ## Appending / overriding Content Security Policy When manipulating content security policy, there are a few things to consider. The default header value is `default-src https:` which corresponds to a default configuration of `{ default_src: %w(https:)}`. #### Append to the policy with a directive other than `default_src` The value of `default_src` is joined with the addition if the it is a [fetch directive](https://w3c.github.io/webappsec-csp/#directives-fetch). Note the `https:` is carried over from the `default-src` config. If you do not want this, use `override_content_security_policy_directives` instead. To illustrate: ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') } end ``` Code | Result ------------- | ------------- `append_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src 'self' mycdn.com` `override_content_security_policy_directives(script_src: %w(mycdn.com))` | `default-src 'self'; script-src mycdn.com` #### Nonce You can use a view helper to automatically add nonces to script tags: ```erb <%= nonced_javascript_tag do %> console.log("nonced!"); <% end %> <%= nonced_style_tag do %> body { background-color: black; } <% end %> ``` becomes: ```html ``` ``` Content-Security-Policy: ... script-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; style-src 'nonce-/jRAxuLJsDXAxqhNBB7gg7h55KETtDQBXe4ZL+xIXwI=' ...; ``` `script`/`style-nonce` can be used to whitelist inline content. To do this, call the `content_security_policy_script_nonce` or `content_security_policy_style_nonce` then set the nonce attributes on the various tags. ```erb ``` secure_headers-3.7.1/CONTRIBUTING.md0000644000004100000410000000352613156767433017011 0ustar www-datawww-data## Contributing [fork]: https://github.com/twitter/secureheaders/fork [pr]: https://github.com/twitter/secureheaders/compare [style]: https://github.com/styleguide/ruby [code-of-conduct]: CODE_OF_CONDUCT.md Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. ## Submitting a pull request 0. [Fork][fork] and clone the repository 0. Configure and install the dependencies: `bundle install` 0. Make sure the tests pass on your machine: `bundle exec rspec spec` 0. Create a new branch: `git checkout -b my-branch-name` 0. Make your change, add tests, and make sure the tests still pass and that no warnings are raised 0. Push to your fork and [submit a pull request][pr] 0. Pat your self on the back and wait for your pull request to be reviewed and merged. Here are a few things you can do that will increase the likelihood of your pull request being accepted: - Write tests. - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). ## Releasing 0. Ensure CI is green 0. Pull the latest code 0. Increment the version 0. Run `gem build darrrr.gemspec` 0. Bump the Gemfile and Gemfile.lock versions for an app which relies on this gem 0. Test behavior locally, branch deploy, whatever needs to happen 0. Run `bundle exec rake release` ## Resources - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) secure_headers-3.7.1/LICENSE0000644000004100000410000000206513156767433015562 0ustar www-datawww-dataCopyright 2013, 2014, 2015, 2016, 2017 Twitter, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. secure_headers-3.7.1/.ruby-gemset0000644000004100000410000000001613156767433017013 0ustar www-datawww-datasecureheaders secure_headers-3.7.1/.github/0000755000004100000410000000000013156767433016112 5ustar www-datawww-datasecure_headers-3.7.1/.github/PULL_REQUEST_TEMPLATE.md0000644000004100000410000000070013156767433021710 0ustar www-datawww-data## All PRs: * [ ] Has tests * [ ] Documentation updated ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? secure_headers-3.7.1/.github/ISSUE_TEMPLATE.md0000644000004100000410000000205313156767433020617 0ustar www-datawww-data# Feature Requests ## Adding a new header Generally, adding a new header is always OK. * Is the header supported by any user agent? If so, which? * What does it do? * What are the valid values for the header? * Where does the specification live? ## Adding a new CSP directive * Is the directive supported by any user agent? If so, which? * What does it do? * What are the valid values for the directive? --- # Bugs Console errors and deprecation warnings are considered bugs that should be addressed with more precise UA sniffing. Bugs caused by incorrect or invalid UA sniffing are also bugs. ### Expected outcome Describe what you expected to happen 1. I configure CSP to do X 1. When I inspect the response headers, the CSP should have included X ### Actual outcome 1. The generated policy did not include X ### Config Please provide the configuration (`SecureHeaders::Configuration.default`) you are using including any overrides (`SecureHeaders::Configuration.override`). ### Generated headers Provide a sample response containing the headers secure_headers-3.7.1/CHANGELOG.md0000644000004100000410000005214413156767433016371 0ustar www-datawww-data## 3.7.1 Fix support for the sandbox attribute of CSP. `true` and `[]` represent the maximally restricted policy (`sandbox;`) and validate other values. ## 3.7.0 Adds support for the `Expect-CT` header (@jacobbednarz: https://github.com/twitter/secureheaders/pull/322) ## 3.6.7 Actually set manifest-src when configured. https://github.com/twitter/secureheaders/pull/339 Thanks @carlosantoniodasilva! ## 3.6.6 wat? ## 3.6.5 Update clear-site-data header to use current format specified by the specification. ## 3.6.4 Fix case where mixing frame-src/child-src dynamically would behave in unexpected ways: https://github.com/twitter/secureheaders/pull/325 ## 3.6.3 Remove deprecation warning when setting `frame-src`. It is no longer deprecated. ## 3.6.2 Now that Safari 10 supports nonces and it appears to work, enable the nonce feature for safari. ## 3.6.1 Improved memory use via minor improvements clever hacks that are sadly needed. Thanks @carlosantoniodasilva! ## 3.6.0 Add support for the clear-site-data header ## 3.5.1 * Fix bug that can occur when useragent library version is older, resulting in a nil version sometimes. * Add constant for `strict-dynamic` ## 3.5.0 This release adds support for setting two CSP headers (enforced/report-only) and management around them. ## 3.4.1 Named Appends ### Small bugfix If your CSP did not define a script/style-src and you tried to use a script/style nonce, the nonce would be added to the page but it would not be added to the CSP. A workaround is to define a script/style src but now it should add the missing directive (and populate it with the default-src). ### Named Appends Named Appends are blocks of code that can be reused and composed during requests. e.g. If a certain partial is rendered conditionally, and the csp needs to be adjusted for that partial, you can create a named append for that situation. The value returned by the block will be passed into `append_content_security_policy_directives`. The current request object is passed as an argument to the block for even more flexibility. ```ruby def show if include_widget? @widget = widget.render use_content_security_policy_named_append(:widget_partial) end end SecureHeaders::Configuration.named_append(:widget_partial) do |request| if request.controller_instance.current_user.in_test_bucket? SecureHeaders.override_x_frame_options(request, "DENY") { child_src: %w(beta.thirdpartyhost.com) } else { child_src: %w(thirdpartyhost.com) } end end ``` You can use as many named appends as you would like per request, but be careful because order of inclusion matters. Consider the following: ```ruby SecureHeader::Configuration.default do |config| config.csp = { default_src: %w('self')} end SecureHeaders::Configuration.named_append(:A) do |request| { default_src: %w(myhost.com) } end SecureHeaders::Configuration.named_append(:B) do |request| { script_src: %w('unsafe-eval') } end ``` The following code will produce different policies due to the way policies are normalized (e.g. providing a previously undefined directive that inherits from `default-src`, removing host source values when `*` is provided. Removing `'none'` when additional values are present, etc.): ```ruby def index use_content_security_policy_named_append(:A) use_content_security_policy_named_append(:B) # produces default-src 'self' myhost.com; script-src 'self' myhost.com 'unsafe-eval'; end def show use_content_security_policy_named_append(:B) use_content_security_policy_named_append(:A) # produces default-src 'self' myhost.com; script-src 'self' 'unsafe-eval'; end ``` ## 3.4.0 the frame-src/child-src transition for Firefox. Handle the `child-src`/`frame-src` transition semi-intelligently across versions. I think the code best descibes the behavior here: ```ruby if supported_directives.include?(:child_src) @config[:child_src] = @config[:child_src] || @config[:frame_src] else @config[:frame_src] = @config[:frame_src] || @config[:child_src] end ``` Also, @koenpunt noticed that we were [loading view helpers](https://github.com/twitter/secureheaders/pull/272) in a way that Rails 5 did not like. ## 3.3.2 minor fix to silence warnings when using rake [@dankohn](https://github.com/twitter/secureheaders/issues/257) was seeing "already initialized" errors in his output. This change conditionally defines the constants. ## 3.3.1 bugfix for boolean CSP directives [@stefansundin](https://github.com/twitter/secureheaders/pull/253) noticed that supplying `false` to "boolean" CSP directives (e.g. `upgrade-insecure-requests` and `block-all-mixed-content`) would still include the value. ## 3.3.0 referrer-policy support While not officially part of the spec and not implemented anywhere, support for the experimental [`referrer-policy` header](https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header) was [preemptively added](https://github.com/twitter/secureheaders/pull/249). Additionally, two minor enhancements were added this version: 1. [Warn when the HPKP report host is the same as the current host](https://github.com/twitter/secureheaders/pull/246). By definition any generated reports would be reporting to a known compromised connection. 1. [Filter unsupported CSP directives when using Edge](https://github.com/twitter/secureheaders/pull/247). Previously, this was causing many warnings in the developer console. ## 3.2.0 Cookie settings and CSP hash sources ### Cookies SecureHeaders supports `Secure`, `HttpOnly` and [`SameSite`](https://tools.ietf.org/html/draft-west-first-party-cookies-07) cookies. These can be defined in the form of a boolean, or as a Hash for more refined configuration. __Note__: Regardless of the configuration specified, Secure cookies are only enabled for HTTPS requests. #### Boolean-based configuration Boolean-based configuration is intended to globally enable or disable a specific cookie attribute. ```ruby config.cookies = { secure: true, # mark all cookies as Secure httponly: false, # do not mark any cookies as HttpOnly } ``` #### Hash-based configuration Hash-based configuration allows for fine-grained control. ```ruby config.cookies = { secure: { except: ['_guest'] }, # mark all but the `_guest` cookie as Secure httponly: { only: ['_rails_session'] }, # only mark the `_rails_session` cookie as HttpOnly } ``` #### SameSite cookie configuration SameSite cookies permit either `Strict` or `Lax` enforcement mode options. ```ruby config.cookies = { samesite: { strict: true # mark all cookies as SameSite=Strict } } ``` `Strict` and `Lax` enforcement modes can also be specified using a Hash. ```ruby config.cookies = { samesite: { strict: { only: ['_rails_session'] }, lax: { only: ['_guest'] } } } ``` #### Hash `script`/`style-src` hashes can be used to whitelist inline content that is static. This has the benefit of allowing inline content without opening up the possibility of dynamic javascript like you would with a `nonce`. You can add hash sources directly to your policy : ```ruby ::SecureHeaders::Configuration.default do |config| config.csp = { default_src: %w('self') # this is a made up value but browsers will show the expected hash in the console. script_src: %w(sha256-123456) } end ``` You can also use the automated inline script detection/collection/computation of hash source values in your app. ```bash rake secure_headers:generate_hashes ``` This will generate a file (`config/secure_headers_generated_hashes.yml` by default, you can override by setting `ENV["secure_headers_generated_hashes_file"]`) containing a mapping of file names with the array of hash values found on that page. When ActionView renders a given file, we check if there are any known hashes for that given file. If so, they are added as values to the header. ```yaml --- scripts: app/views/asdfs/index.html.erb: - "'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg='" styles: app/views/asdfs/index.html.erb: - "'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY='" - "'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE='" ``` ##### Helpers **This will not compute dynamic hashes** by design. The output of both helpers will be a plain `script`/`style` tag without modification and the known hashes for a given file will be added to `script-src`/`style-src` when `hashed_javascript_tag` and `hashed_style_tag` are used. You can use `raise_error_on_unrecognized_hash = true` to be extra paranoid that you have precomputed hash values for all of your inline content. By default, this will raise an error in non-production environments. ```erb <%= hashed_style_tag do %> body { background-color: black; } <% end %> <%= hashed_style_tag do %> body { font-size: 30px; font-color: green; } <% end %> <%= hashed_javascript_tag do %> console.log(1) <% end %> ``` ``` Content-Security-Policy: ... script-src 'sha256-yktKiAsZWmc8WpOyhnmhQoDf9G2dAZvuBBC+V0LGQhg=' ... ; style-src 'sha256-SLp6LO3rrKDJwsG9uJUxZapb4Wp2Zhj6Bu3l+d9rnAY=' 'sha256-HSGHqlRoKmHAGTAJ2Rq0piXX4CnEbOl1ArNd6ejp2TE=' ...; ``` ## 3.1.2 Bug fix for regression See https://github.com/twitter/secureheaders/pull/239 This meant that when header caches were regenerated upon calling `SecureHeaders.override(:name)` and using it with `use_secure_headers_override` would result in default values for anything other than CSP/HPKP. ## 3.1.1 Bug fix for regression See https://github.com/twitter/secureheaders/pull/235 `idempotent_additions?` would return false when comparing `OPT_OUT` with `OPT_OUT`, causing `header_hash_for` to return a header cache with `{ nil => nil }` which cause the middleware to blow up when `{ nil => nil }` was merged into the rack header hash. This is a regression in 3.1.0 only. Now it returns true. I've added a test case to ensure that `header_hash_for` will never return such an element. ## 3.1.0 Adding secure cookie support New feature: marking all cookies as secure. Added by @jmera in https://github.com/twitter/secureheaders/pull/231. In the future, we'll probably add the ability to whitelist individual cookies that should not be marked secure. PRs welcome. Internal refactoring: In https://github.com/twitter/secureheaders/pull/232, we changed the way dynamic CSP is handled internally. The biggest benefit is that highly dynamic policies (which can happen with multiple `append/override` calls per request) are handled better: 1. Only the CSP header cache is busted when using a dynamic policy. All other headers are preserved and don't need to be generated. Dynamic X-Frame-Options changes modify the cache directly. 1. Idempotency checks for policy modifications are deferred until the end of the request lifecycle and only happen once, instead of per `append/override` call. The idempotency check itself is fairly expensive itself. 1. CSP header string is produced at most once per request. ## 3.0.3 Bug fix for handling policy merges where appending a non-default source value (report-uri, plugin-types, frame-ancestors, base-uri, and form-action) would be combined with the default-src value. Appending a directive that doesn't exist in the current policy combines the new value with `default-src` to mimic the actual behavior of the addition. However, this does not make sense for non-default-src values (a.k.a. "fetch directives") and can lead to unexpected behavior like a `report-uri` value of `*`. Previously, this config: ``` { default_src => %w(*) } ``` When appending: ``` { report_uri => %w(https://report-uri.io/asdf) } ``` Would result in `default-src *; report-uri *` which doesn't make any sense at all. ## 3.0.2 Bug fix for handling CSP configs that supply a frozen hash. If a directive value is `nil`, then appending to a config with a frozen hash would cause an error since we're trying to modify a frozen hash. See https://github.com/twitter/secureheaders/pull/223. ## 3.0.1 Adds `upgrade-insecure-requests` support for requests from Firefox and Chrome (and Opera). See [the spec](https://www.w3.org/TR/upgrade-insecure-requests/) for details. ## 3.0.0 secure_headers 3.0.0 is a near-complete, not-entirely-backward-compatible rewrite. Please see the [upgrade guide](https://github.com/twitter/secureheaders/blob/master/upgrading-to-3-0.md) for an in-depth explanation of the changes and the suggested upgrade path. ## 2.5.1 - 2016-02-16 18:11:11 UTC - Remove noisy deprecation warning See https://github.com/twitter/secureheaders/issues/203 and https://github.com/twitter/secureheaders/commit/cfad0e52285353b88e46fe384e7cd60bf2a01735 >> Upon upgrading to secure_headers 2.5.0, I get a flood of these deprecations when running my tests: > [DEPRECATION] secure_header_options_for will not be supported in secure_headers /cc @bquorning ## 2.5.0 - 2016-01-06 22:11:02 UTC - 2.x deprecation warning release This release contains deprecation warnings for those wishing to upgrade to the 3.x series. With this release, fixing all deprecation warnings will make your configuration compatible when you decide to upgrade to the soon-to-be-released 3.x series (currently in pre-release stage). No changes to functionality should be observed unless you were using procs as CSP config values. ## 2.4.4 - 2015-12-03 23:29:42 UTC - Bug fix release If you use the `header_hash` method for setting your headers in middleware and you opted out of a header (via setting the value to `false`), you would run into an exception as described in https://github.com/twitter/secureheaders/pull/193 ``` NoMethodError: undefined method `name' for nil:NilClass # ./lib/secure_headers.rb:63:in `block in header_hash' # ./lib/secure_headers.rb:54:in `each' # ./lib/secure_headers.rb:54:in `inject' # ./lib/secure_headers.rb:54:in `header_hash' ``` ## 2.4.3 - 2015-10-23 18:35:43 UTC - Performance improvement @igrep reported an anti-patter in use regarding [UserAgentParser](https://github.com/ua-parser/uap-ruby). This caused UserAgentParser to reload it's entire configuration set *twice** per request. Moving this to a cached constant prevents the constant reinstantiation and will improve performance. https://github.com/twitter/secureheaders/issues/187 ## 2.4.2 - 2015-10-20 20:22:08 UTC - Bug fix release A nasty regression meant that many CSP configuration values were "reset" after the first request, one of these being the "enforce" flag. See https://github.com/twitter/secureheaders/pull/184 for the full list of fields that were affected. Thanks to @spdawson for reporting this https://github.com/twitter/secureheaders/issues/183 ## 2.4.1 - 2015-10-14 22:57:41 UTC - More UA sniffing This release may change the output of headers based on per browser support. Unsupported directives will be omitted based on the user agent per request. See https://github.com/twitter/secureheaders/pull/179 p.s. this will likely be the last non-bugfix release for the 2.x line. 3.x will be a major change. Sneak preview: https://github.com/twitter/secureheaders/pull/181 ## 2.4.0 - 2015-10-01 23:05:38 UTC - Some internal changes affecting behavior, but not functionality If you leveraged `secure_headers` automatic filling of empty directives, the header value will change but it should not affect how the browser applies the policy. The content of CSP reports may change if you do not update your policy. before === ```ruby config.csp = { :default_src => "'self'" } ``` would produce `default-src 'self'; connect-src 'self'; frame-src 'self' ... etc.` after === ```ruby config.csp = { :default_src => "'self'" } ``` will produce `default-src 'self'` The reason for this is that a `default-src` violation was basically impossible to handle. Chrome sends an `effective-directive` which helps indicate what kind of violation occurred even if it fell back to `default-src`. This is part of the [CSP Level 2 spec](http://www.w3.org/TR/CSP2/#violation-report-effective-directive) so hopefully other browsers will implement this soon. Workaround === Just set the values yourself, but really a `default-src` of anything other than `'none'` implies the policy can be tightened dramatically. "ZOMG don't you work for github and doesn't github send a `default-src` of `*`???" Yes, this is true. I disagree with this but at the same time, github defines every single known directive that a browser supports so `default-src` will only apply if a new directive is introduced, and we'd rather fail open. For now. ```ruby config.csp = { :default_src => "'self'", :connect_src => "'self'", :frame_src => "'self'" ... etc. } ``` Besides, relying on `default-src` is often not what you want and encourages an overly permissive policy. I've seen it. Seriously. `default-src 'unsafe-inline' 'unsafe-eval' https: http:;` That's terrible. ## 2.3.0 - 2015-09-30 19:43:09 UTC - Add header_hash feature for use in middleware. See https://github.com/twitter/secureheaders/issues/167 and https://github.com/twitter/secureheaders/pull/168 tl;dr is that there is a class method `SecureHeaders::header_hash` that will return a hash of header name => value pairs useful for merging with the rack header hash in middleware. ## 2.2.4 - 2015-08-26 23:31:37 UTC - Print deprecation warning for 1.8.7 users As discussed in https://github.com/twitter/secureheaders/issues/154 ## 2.2.3 - 2015-08-14 20:26:12 UTC - Adds ability to opt-out of automatically adding data: sources to img-src See https://github.com/twitter/secureheaders/pull/161 ## 2.2.2 - 2015-07-02 21:18:38 UTC - Another option for config granularity. See https://github.com/twitter/secureheaders/pull/147 Allows you to override a controller method that returns a config in the context of the executing action. ## 2.2.1 - 2015-06-24 21:01:57 UTC - When using nonces, do not include the nonce for safari / IE See https://github.com/twitter/secureheaders/pull/150 Safari will generate a warning that it doesn't support nonces. Safari will fall back to the `unsafe-inline`. Things will still work, but an ugly message is printed to the console. This opts out safari and IE users from the inline script protection. I haven't verified any IE behavior yet, so I'm just assuming it doesn't work. ## 2.2.0 - 2015-06-18 22:01:23 UTC - Pass controller reference to callable config value expressions. https://github.com/twitter/secureheaders/pull/148 Facilitates better per-request config: `:enforce => lambda { |controller| controller.current_user.beta_testing? }` **NOTE** if you used `lambda` config values, this will raise an exception until you add the controller reference: bad: `lambda { true }` good: `lambda { |controller| true }` `proc { true }` `proc { |controller| true }` ## v2.1.0 - 2015-05-07 18:34:56 UTC - Add hpkp support Includes https://github.com/twitter/secureheaders/pull/143 (which is really just https://github.com/twitter/secureheaders/pull/132) from @thirstscolr ## v2.0.2 - 2015-05-05 03:09:44 UTC - Add report_uri constant value Just a small change that adds a constant that was missing as reported in https://github.com/twitter/secureheaders/issues/141 ## v2.0.1 - 2015-03-20 18:46:47 UTC - View Helpers Fixed Fixes an issue where view helpers (for nonces, hashes, etc) weren't available in views. ## 2.0.0 - 2015-01-23 20:23:56 UTC - 2.0 This release contains support for more csp level 2 features such as the new directives, the script hash integration, and more. It also sets a new header by default: `X-Permitted-Cross-Domain-Policies` Support for hpkp is not included in this release as the implementations are still very unstable. :rocket: ## v.2.0.0.pre2 - 2014-12-06 01:55:42 UTC - Adds X-Permitted-Cross-Domain-Policies support by default The only change between this and the first pre release is that the X-Permitted-Cross-Domain-Policies support is included. ## v1.4.0 - 2014-12-06 01:54:48 UTC - Deprecate features in preparation for 2.0 This removes the forwarder and "experimental" feature. The forwarder wasn't well maintained and created a lot of headaches. Also, it was using an outdated certificate pack for compatibility. That's bad. The experimental feature wasn't really used and it complicated the codebase a lot. It's also a questionably useful API that is very confusing. ## v2.0.0.pre - 2014-11-14 00:54:07 UTC - 2.0.0.pre - CSP level 2 support This release is intended to be ready for CSP level 2. Mainly, this means there is direct support for hash/nonce of inline content and includes many new directives (which do not inherit from default-src) ## v1.3.4 - 2014-10-13 22:05:44 UTC - * Adds X-Download-Options support * Adds support for X-XSS-Protection reporting * Defers loading of rails engine for faster boot times ## v1.3.3 - 2014-08-15 02:30:24 UTC - hsts preload confirmation value support @agl just made a new option for HSTS representing confirmation that a site wants to be included in a browser's preload list (https://hstspreload.appspot.com). This just adds a new 'preload' option to the HSTS settings to specify that option. ## v1.3.2 - 2014-08-14 00:01:32 UTC - Add app tagging support Tagging Requests It's often valuable to send extra information in the report uri that is not available in the reports themselves. Namely, "was the policy enforced" and "where did the report come from" ```ruby { :tag_report_uri => true, :enforce => true, :app_name => 'twitter', :report_uri => 'csp_reports' } ``` Results in ``` report-uri csp_reports?enforce=true&app_name=twitter ``` secure_headers-3.7.1/README.md0000644000004100000410000002136513156767433016040 0ustar www-datawww-data# Secure Headers [![Build Status](https://travis-ci.org/twitter/secureheaders.svg?branch=master)](http://travis-ci.org/twitter/secureheaders) [![Code Climate](https://codeclimate.com/github/twitter/secureheaders.svg)](https://codeclimate.com/github/twitter/secureheaders) [![Coverage Status](https://coveralls.io/repos/twitter/secureheaders/badge.svg)](https://coveralls.io/r/twitter/secureheaders) **The 3.x branch was recently merged**. See the [upgrading to 3.x doc](upgrading-to-3-0.md) for instructions on how to upgrade including the differences and benefits of using the 3.x branch. **The [2.x branch](https://github.com/twitter/secureheaders/tree/2.x) will be maintained**. The documentation below only applies to the 3.x branch. See the 2.x [README](https://github.com/twitter/secureheaders/blob/2.x/README.md) for the old way of doing things. The gem will automatically apply several headers that are related to security. This includes: - Content Security Policy (CSP) - Helps detect/prevent XSS, mixed-content, and other classes of attack. [CSP 2 Specification](http://www.w3.org/TR/CSP2/) - https://csp.withgoogle.com - https://csp.withgoogle.com/docs/strict-csp.html - https://csp-evaluator.withgoogle.com - HTTP Strict Transport Security (HSTS) - Ensures the browser never visits the http version of a website. Protects from SSLStrip/Firesheep attacks. [HSTS Specification](https://tools.ietf.org/html/rfc6797) - X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options Specification](https://tools.ietf.org/html/rfc7034) - X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](https://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx) - X-Content-Type-Options - [Prevent content type sniffing](https://msdn.microsoft.com/library/gg622941\(v=vs.85\).aspx) - X-Download-Options - [Prevent file downloads opening](https://msdn.microsoft.com/library/jj542450(v=vs.85).aspx) - 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). `secure_headers` is a library with a global config, per request overrides, and rack middleware that enables you customize your application settings. ## Documentation - [Named overrides and appends](docs/named_overrides_and_appends.md) - [Per action configuration](docs/per_action_configuration.md) - [Cookies](docs/cookies.md) - [HPKP](docs/HPKP.md) - [Hashes](docs/hashes.md) - [Sinatra Config](docs/sinatra.md) ## Getting Started ### Rails 3+ For Rails 3+ applications, `secure_headers` has a `railtie` that should automatically include the middleware. If for some reason the middleware is not being included follow the instructions for Rails 2. ### Rails 2 For Rails 2 or non-rails applications, an explicit statement is required to use the middleware component. ```ruby use SecureHeaders::Middleware ``` ## Configuration If you do not supply a `default` configuration, exceptions will be raised. If you would like to use a default configuration (which is fairly locked down), just call `SecureHeaders::Configuration.default` without any arguments or block. All `nil` values will fallback to their default values. `SecureHeaders::OPT_OUT` will disable the header entirely. ```ruby SecureHeaders::Configuration.default do |config| config.cookies = { secure: true, # mark all cookies as "Secure" httponly: true, # mark all cookies as "HttpOnly" samesite: { lax: true # mark all cookies as SameSite=lax } } # Add "; preload" and submit the site to hstspreload.org for best protection. config.hsts = "max-age=#{20.years.to_i}; includeSubdomains" config.x_frame_options = "DENY" config.x_content_type_options = "nosniff" config.x_xss_protection = "1; mode=block" config.x_download_options = "noopen" config.x_permitted_cross_domain_policies = "none" config.referrer_policy = "origin-when-cross-origin" config.clear_site_data = [ "cache", "cookies", "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] preserve_schemes: true, # default: false. Schemes are removed from host sources to save bytes and discourage mixed content. # directive values: these values will directly translate into source directives default_src: %w(https: 'self'), base_uri: %w('self'), block_all_mixed_content: true, # see http://www.w3.org/TR/mixed-content/ child_src: %w('self'), # if child-src isn't supported, the value for frame-src will be set. connect_src: %w(wss:), font_src: %w('self' data:), form_action: %w('self' github.com), frame_ancestors: %w('none'), img_src: %w(mycdn.com data:), manifest_src: %w('self'), media_src: %w(utoob.com), object_src: %w('self'), plugin_types: %w(application/x-shockwave-flash), script_src: %w('self'), style_src: %w('unsafe-inline'), upgrade_insecure_requests: true, # see https://www.w3.org/TR/upgrade-insecure-requests/ report_uri: %w(https://report-uri.io/example-csp) } # This is available only from 3.5.0; use the `report_only: true` setting for 3.4.1 and below. config.csp_report_only = config.csp.merge({ img_src: %w(somewhereelse.com), report_uri: %w(https://report-uri.io/example-csp-report-only) }) config.hpkp = { report_only: false, max_age: 60.days.to_i, include_subdomains: true, report_uri: "https://report-uri.io/example-hpkp", pins: [ {sha256: "abc"}, {sha256: "123"} ] } end ``` ## Default values All headers except for PublicKeyPins and ClearSiteData have a default value. The default set of headers is: ``` Content-Security-Policy: default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline' Strict-Transport-Security: max-age=631138519 X-Content-Type-Options: nosniff X-Download-Options: noopen X-Frame-Options: sameorigin X-Permitted-Cross-Domain-Policies: none X-Xss-Protection: 1; mode=block ``` ### Default CSP By default, the above CSP will be applied to all requests. If you **only** want to set a Report-Only header, opt-out of the default enforced header for clarity. The configuration will assume that if you only supply `csp_report_only` that you intended to opt-out of `csp` but that's for the sake of backwards compatibility and it will be removed in the future. ```ruby Configuration.default do |config| config.csp = SecureHeaders::OPT_OUT # If this line is omitted, we will assume you meant to opt out. config.csp_report_only = { default_src: %w('self') } end ``` ## Similar libraries * Rack [rack-secure_headers](https://github.com/frodsan/rack-secure_headers) * Node.js (express) [helmet](https://github.com/helmetjs/helmet) and [hood](https://github.com/seanmonstar/hood) * Node.js (hapi) [blankie](https://github.com/nlf/blankie) * J2EE Servlet >= 3.0 [headlines](https://github.com/sourceclear/headlines) * ASP.NET - [NWebsec](https://github.com/NWebsec/NWebsec/wiki) * Python - [django-csp](https://github.com/mozilla/django-csp) + [commonware](https://github.com/jsocol/commonware/); [django-security](https://github.com/sdelements/django-security) * Go - [secureheader](https://github.com/kr/secureheader) * Elixir [secure_headers](https://github.com/anotherhale/secure_headers) * Dropwizard [dropwizard-web-security](https://github.com/palantir/dropwizard-web-security) * Ember.js [ember-cli-content-security-policy](https://github.com/rwjblue/ember-cli-content-security-policy/) * PHP [secure-headers](https://github.com/BePsvPT/secure-headers) ## License Copyright 2013-2014 Twitter, Inc and other contributors. Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 secure_headers-3.7.1/Guardfile0000644000004100000410000000056413156767433016404 0ustar www-datawww-dataguard :rspec, cmd: "bundle exec rspec", all_on_start: true, all_after_pass: true do require "guard/rspec/dsl" dsl = Guard::RSpec::Dsl.new(self) # RSpec files rspec = dsl.rspec watch(rspec.spec_helper) { rspec.spec_dir } watch(rspec.spec_support) { rspec.spec_dir } watch(rspec.spec_files) watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } end