Skip to content

Commit c6e263d

Browse files
committed
Merge pull request #123 from stve/x-permitted-cross-domain-policies
X-Permitted-Cross-Domain-Policies support
2 parents c3c0479 + de2e8c0 commit c6e263d

File tree

11 files changed

+170
-3
lines changed

11 files changed

+170
-3
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The gem will automatically apply several headers that are related to security.
66
- X-Frame-Options (XFO) - Prevents your content from being framed and potentially clickjacked. [X-Frame-Options draft](https://tools.ietf.org/html/draft-ietf-websec-x-frame-options-02)
77
- X-XSS-Protection - [Cross site scripting heuristic filter for IE/Chrome](http://msdn.microsoft.com/en-us/library/dd565647\(v=vs.85\).aspx)
88
- X-Content-Type-Options - [Prevent content type sniffing](http://msdn.microsoft.com/en-us/library/ie/gg622941\(v=vs.85\).aspx)
9+
- X-Download-Options - [Prevent file downloads opening](http://msdn.microsoft.com/en-us/library/ie/jj542450(v=vs.85).aspx)
10+
- 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)
911

1012
This gem has integration with Rails, but works for any Ruby code. See the sinatra example section.
1113

@@ -48,6 +50,8 @@ The following methods are going to be called, unless they are provided in a `ski
4850
* `:set_x_frame_options_header`
4951
* `:set_x_xss_protection_header`
5052
* `:set_x_content_type_options_header`
53+
* `:set_x_download_options_header`
54+
* `:set_x_permitted_cross_domain_policies_header`
5155

5256
### Bonus Features
5357

@@ -65,6 +69,8 @@ This gem makes a few assumptions about how you will use some features. For exam
6569
config.x_frame_options = 'DENY'
6670
config.x_content_type_options = "nosniff"
6771
config.x_xss_protection = {:value => 1, :mode => 'block'}
72+
config.x_download_options = 'noopen'
73+
config.x_permitted_cross_domain_policies = 'none'
6874
config.csp = {
6975
:default_src => "https: self",
7076
:frame_src => "https: http:.twimg.com http://itunes.apple.com",
@@ -106,6 +112,8 @@ This configuration will likely work for most applications without modification.
106112
:x_frame_options => {:value => 'SAMEORIGIN'}
107113
:x_xss_protection => {:value => 1, :mode => 'block'} # set the :mode option to false to use "warning only" mode
108114
:x_content_type_options => {:value => 'nosniff'}
115+
:x_download_options => {:value => 'noopen'}
116+
:x_permitted_cross_domain_policies => {:value => 'none'}
109117
```
110118

111119
### Content Security Policy (CSP)
@@ -335,6 +343,8 @@ require 'secure_headers'
335343
config.x_frame_options = 'DENY'
336344
config.x_content_type_options = "nosniff"
337345
config.x_xss_protection = {:value => 1, :mode => false}
346+
config.x_download_options = 'noopen'
347+
config.x_permitted_cross_domain_policies = 'none'
338348
config.csp = {
339349
:default_src => "https: inline eval",
340350
:report_uri => '//example.com/uri-directive',
@@ -390,6 +400,8 @@ def before_load
390400
config.x_frame_options = 'DENY'
391401
config.x_content_type_options = "nosniff"
392402
config.x_xss_protection = {:value => '1', :mode => false}
403+
config.x_download_options = 'noopen'
404+
config.x_permitted_cross_domain_policies = 'none'
393405
config.csp = {
394406
:default_src => "https: inline eval",
395407
:report_uri => '//example.com/uri-directive',

fixtures/rails_3_2_12/config/initializers/secure_headers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
config.x_frame_options = 'SAMEORIGIN'
44
config.x_content_type_options = "nosniff"
55
config.x_xss_protection = {:value => 1, :mode => 'block'}
6+
config.x_permitted_cross_domain_policies = 'none'
67
csp = {
78
:default_src => "self",
89
:script_src => "self nonce",

fixtures/rails_3_2_12/spec/controllers/other_things_controller_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ def request(opts = {})
6767
expect(@env['X-Content-Type-Options']).to eq("nosniff")
6868
end
6969

70+
it "sets the X-Permitted-Cross-Domain-Policies" do
71+
get '/'
72+
expect(@env['X-Permitted-Cross-Domain-Policies']).to eq("none")
73+
end
74+
7075
context "using IE" do
7176
it "sets the X-Content-Type-Options header" do
7277
@env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"

fixtures/rails_3_2_12/spec/controllers/things_controller_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
expect(response.headers['X-Content-Type-Options']).to eq("nosniff")
3939
end
4040

41+
it "sets the X-Permitted-Cross-Domain-Policies" do
42+
get :index
43+
expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none")
44+
end
45+
4146
context "using IE" do
4247
it "sets the X-Content-Type-Options header" do
4348
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"

fixtures/rails_3_2_12_no_init/spec/controllers/other_things_controller_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
3535
end
3636

37+
it "sets the X-Permitted-Cross-Domain-Policies" do
38+
get :index
39+
expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none")
40+
end
41+
3742
context "using IE" do
3843
it "sets the X-Content-Type-Options header" do
3944
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"

fixtures/rails_3_2_12_no_init/spec/controllers/things_controller_spec.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@
3838
expect(response.headers['X-Content-Type-Options']).to eq(SecureHeaders::XContentTypeOptions::Constants::DEFAULT_VALUE)
3939
end
4040

41+
it "sets the X-Permitted-Cross-Domain-Policies" do
42+
get :index
43+
expect(response.headers['X-Permitted-Cross-Domain-Policies']).to eq("none")
44+
end
45+
4146
context "using IE" do
4247
it "sets the X-Content-Type-Options header" do
4348
request.env['HTTP_USER_AGENT'] = "Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0"

lib/secure_headers.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ module SecureHeaders
55
module Configuration
66
class << self
77
attr_accessor :hsts, :x_frame_options, :x_content_type_options,
8-
:x_xss_protection, :csp, :x_download_options, :script_hashes
8+
:x_xss_protection, :csp, :x_download_options, :script_hashes,
9+
:x_permitted_cross_domain_policies
910

1011
def configure &block
1112
instance_eval &block
@@ -46,6 +47,7 @@ def ensure_security_headers options = {}
4647
before_filter :set_x_xss_protection_header
4748
before_filter :set_x_content_type_options_header
4849
before_filter :set_x_download_options_header
50+
before_filter :set_x_permitted_cross_domain_policies_header
4951
end
5052

5153
# we can't use ||= because I'm overloading false => disable, nil => default
@@ -63,6 +65,7 @@ def set_security_headers(options = self.class.secure_headers_options)
6365
set_x_xss_protection_header(options[:x_xss_protection])
6466
set_x_content_type_options_header(options[:x_content_type_options])
6567
set_x_download_options_header(options[:x_download_options])
68+
set_x_permitted_cross_domain_policies_header(options[:x_permitted_cross_domain_policies])
6669
end
6770

6871
# set_csp_header - uses the request accessor and SecureHeader::Configuration settings
@@ -137,6 +140,10 @@ def set_x_download_options_header(options=self.class.secure_headers_options[:x_d
137140
set_a_header(:x_download_options, XDownloadOptions, options)
138141
end
139142

143+
def set_x_permitted_cross_domain_policies_header(options=self.class.secure_headers_options[:x_permitted_cross_domain_policies])
144+
set_a_header(:x_permitted_cross_domain_policies, XPermittedCrossDomainPolicies, options)
145+
end
146+
140147
private
141148

142149
def set_a_header(name, klass, options=nil)
@@ -167,6 +174,7 @@ def set_header(name_or_header, value=nil)
167174
require "secure_headers/headers/x_xss_protection"
168175
require "secure_headers/headers/x_content_type_options"
169176
require "secure_headers/headers/x_download_options"
177+
require "secure_headers/headers/x_permitted_cross_domain_policies"
170178
require "secure_headers/railtie"
171179
require "secure_headers/hash_helper"
172180
require "secure_headers/view_helper"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
module SecureHeaders
2+
class XPCDPBuildError < StandardError; end
3+
class XPermittedCrossDomainPolicies < Header
4+
module Constants
5+
XPCDP_HEADER_NAME = "X-Permitted-Cross-Domain-Policies"
6+
DEFAULT_VALUE = 'none'
7+
VALID_POLICIES = %w(all none master-only by-content-type by-ftp-filename)
8+
end
9+
include Constants
10+
11+
def initialize(config = nil)
12+
@config = config
13+
validate_config unless @config.nil?
14+
end
15+
16+
def name
17+
XPCDP_HEADER_NAME
18+
end
19+
20+
def value
21+
case @config
22+
when NilClass
23+
DEFAULT_VALUE
24+
when String
25+
@config
26+
else
27+
@config[:value]
28+
end
29+
end
30+
31+
private
32+
33+
def validate_config
34+
value = @config.is_a?(Hash) ? @config[:value] : @config
35+
unless VALID_POLICIES.include?(value.downcase)
36+
raise XPCDPBuildError.new("Value can only be one of #{VALID_POLICIES.join(', ')}")
37+
end
38+
end
39+
end
40+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
module SecureHeaders
2+
describe XPermittedCrossDomainPolicies do
3+
specify { expect(XPermittedCrossDomainPolicies.new.name).to eq(XPermittedCrossDomainPolicies::Constants::XPCDP_HEADER_NAME)}
4+
specify { expect(XPermittedCrossDomainPolicies.new.value).to eq("none")}
5+
specify { expect(XPermittedCrossDomainPolicies.new('master-only').value).to eq('master-only')}
6+
specify { expect(XPermittedCrossDomainPolicies.new(:value => 'master-only').value).to eq('master-only') }
7+
8+
context "valid configuration values" do
9+
it "accepts 'all'" do
10+
expect {
11+
XPermittedCrossDomainPolicies.new("all")
12+
}.not_to raise_error
13+
14+
expect {
15+
XPermittedCrossDomainPolicies.new(:value => "all")
16+
}.not_to raise_error
17+
end
18+
19+
it "accepts 'by-ftp-filename'" do
20+
expect {
21+
XPermittedCrossDomainPolicies.new("by-ftp-filename")
22+
}.not_to raise_error
23+
24+
expect {
25+
XPermittedCrossDomainPolicies.new(:value => "by-ftp-filename")
26+
}.not_to raise_error
27+
end
28+
29+
it "accepts 'by-content-type'" do
30+
expect {
31+
XPermittedCrossDomainPolicies.new("by-content-type")
32+
}.not_to raise_error
33+
34+
expect {
35+
XPermittedCrossDomainPolicies.new(:value => "by-content-type")
36+
}.not_to raise_error
37+
end
38+
it "accepts 'master-only'" do
39+
expect {
40+
XPermittedCrossDomainPolicies.new("master-only")
41+
}.not_to raise_error
42+
43+
expect {
44+
XPermittedCrossDomainPolicies.new(:value => "master-only")
45+
}.not_to raise_error
46+
end
47+
48+
it "accepts nil" do
49+
expect {
50+
XPermittedCrossDomainPolicies.new
51+
}.not_to raise_error
52+
end
53+
end
54+
55+
context 'invlaid configuration values' do
56+
57+
it "doesn't accept invalid values" do
58+
expect {
59+
XPermittedCrossDomainPolicies.new("open")
60+
}.to raise_error
61+
end
62+
end
63+
end
64+
end

spec/lib/secure_headers_spec.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class DummyClass
1818
allow(subject).to receive(:request).and_return(request)
1919
end
2020

21-
ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection].map{|header| [header, false]}]
21+
ALL_HEADERS = Hash[[:hsts, :csp, :x_frame_options, :x_content_type_options, :x_xss_protection, :x_permitted_cross_domain_policies].map{|header| [header, false]}]
2222

2323
def stub_user_agent val
2424
allow(request).to receive_message_chain(:env, :[]).and_return(val)
@@ -36,6 +36,7 @@ def reset_config
3636
config.x_xss_protection = nil
3737
config.csp = nil
3838
config.x_download_options = nil
39+
config.x_permitted_cross_domain_policies = nil
3940
end
4041
end
4142

@@ -46,6 +47,7 @@ def set_security_headers(subject)
4647
subject.set_x_content_type_options_header
4748
subject.set_x_xss_protection_header
4849
subject.set_x_download_options_header
50+
subject.set_x_permitted_cross_domain_policies_header
4951
end
5052

5153
describe "#set_header" do
@@ -64,14 +66,15 @@ def set_security_headers(subject)
6466
USER_AGENTS.each do |name, useragent|
6567
it "sets all default headers for #{name} (smoke test)" do
6668
stub_user_agent(useragent)
67-
number_of_headers = 6
69+
number_of_headers = 7
6870
expect(subject).to receive(:set_header).exactly(number_of_headers).times # a request for a given header
6971
subject.set_csp_header
7072
subject.set_x_frame_options_header
7173
subject.set_hsts_header
7274
subject.set_x_xss_protection_header
7375
subject.set_x_content_type_options_header
7476
subject.set_x_download_options_header
77+
subject.set_x_permitted_cross_domain_policies_header
7578
end
7679
end
7780

@@ -96,6 +99,11 @@ def set_security_headers(subject)
9699
subject.set_x_frame_options_header(false)
97100
end
98101

102+
it "does not set the X-Permitted-Cross-Domain-Policies header if disabled" do
103+
should_not_assign_header(XPCDP_HEADER_NAME)
104+
subject.set_x_permitted_cross_domain_policies_header(false)
105+
end
106+
99107
it "does not set the HSTS header if disabled" do
100108
should_not_assign_header(HSTS_HEADER_NAME)
101109
subject.set_hsts_header(false)
@@ -133,6 +141,7 @@ def set_security_headers(subject)
133141
config.x_xss_protection = false
134142
config.csp = false
135143
config.x_download_options = false
144+
config.x_permitted_cross_domain_policies = false
136145
end
137146
expect(subject).not_to receive(:set_header)
138147
set_security_headers(subject)
@@ -249,4 +258,16 @@ def set_security_headers(subject)
249258
end
250259
end
251260
end
261+
262+
describe "#set_x_permitted_cross_domain_policies_header" do
263+
it "sets the X-Permitted-Cross-Domain-Policies header" do
264+
should_assign_header(XPCDP_HEADER_NAME, SecureHeaders::XPermittedCrossDomainPolicies::Constants::DEFAULT_VALUE)
265+
subject.set_x_permitted_cross_domain_policies_header
266+
end
267+
268+
it "allows a custom X-Permitted-Cross-Domain-Policies header" do
269+
should_assign_header(XPCDP_HEADER_NAME, "master-only")
270+
subject.set_x_permitted_cross_domain_policies_header(:value => 'master-only')
271+
end
272+
end
252273
end

0 commit comments

Comments
 (0)