Skip to content

Commit bf19b87

Browse files
jacobbednarzjeremy
authored andcommitted
Adds support for configuring HTTP Feature Policy (rails#33439)
A HTTP feature policy is Yet Another HTTP header for instructing the browser about which features the application intends to make use of and to lock down access to others. This is a new security mechanism that ensures that should an application become compromised or a third party attempts an unexpected action, the browser will override it and maintain the intended UX. WICG specification: https://wicg.github.io/feature-policy/ The end result is a HTTP header that looks like the following: ``` Feature-Policy: geolocation 'none'; autoplay https://example.com ``` This will prevent the browser from using geolocation and only allow autoplay on `https://example.com`. Full feature list can be found over in the WICG repository[1]. As of today Chrome and Safari have public support[2] for this functionality with Firefox working on support[3] and Edge still pending acceptance of the suggestion[4]. #### Examples Using an initializer ```rb # config/initializers/feature_policy.rb Rails.application.config.feature_policy do |f| f.geolocation :none f.camera :none f.payment "https://secure.example.com" f.fullscreen :self end ``` In a controller ```rb class SampleController < ApplicationController def index feature_policy do |f| f.geolocation "https://example.com" end end end ``` Some of you might realise that the HTTP feature policy looks pretty close to that of a Content Security Policy; and you're right. So much so that I used the Content Security Policy DSL from rails#31162 as the starting point for this change. This change *doesn't* introduce support for defining a feature policy on an iframe and this has been intentionally done to split the HTTP header and the HTML element (`iframe`) support. If this is successful, I'll look to add that on it's own. Full documentation on HTTP feature policies can be found at https://wicg.github.io/feature-policy/. Google have also published[5] a great in-depth write up of this functionality. [1]: https://github.com/WICG/feature-policy/blob/master/features.md [2]: https://www.chromestatus.com/feature/5694225681219584 [3]: https://bugzilla.mozilla.org/show_bug.cgi?id=1390801 [4]: https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/33507907-support-feature-policy [5]: https://developers.google.com/web/updates/2018/06/feature-policy
1 parent 2fa21fe commit bf19b87

File tree

14 files changed

+608
-1
lines changed

14 files changed

+608
-1
lines changed

actionpack/CHANGELOG.md

+33
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
* Add DSL for configuring HTTP Feature Policy
2+
3+
This new DSL provides a way to configure a HTTP Feature Policy at a
4+
global or per-controller level. Full details of HTTP Feature Policy
5+
specification and guidelines can be found at MDN:
6+
7+
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
8+
9+
Example global policy
10+
11+
```
12+
Rails.application.config.feature_policy do |f|
13+
f.camera :none
14+
f.gyroscope :none
15+
f.microphone :none
16+
f.usb :none
17+
f.fullscreen :self
18+
f.payment :self, "https://secure-example.com"
19+
end
20+
```
21+
22+
Example controller level policy
23+
24+
```
25+
class PagesController < ApplicationController
26+
feature_policy do |p|
27+
p.geolocation "https://example.com"
28+
end
29+
end
30+
```
31+
32+
*Jacob Bednarz*
33+
134
* Add the ability to set the CSP nonce only to the specified directives.
235
336
Fixes #35137.

actionpack/lib/action_controller.rb

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module ActionController
2828
autoload :DefaultHeaders
2929
autoload :EtagWithTemplateDigest
3030
autoload :EtagWithFlash
31+
autoload :FeaturePolicy
3132
autoload :Flash
3233
autoload :ForceSSL
3334
autoload :Head

actionpack/lib/action_controller/base.rb

+1
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ def self.without_modules(*modules)
226226
FormBuilder,
227227
RequestForgeryProtection,
228228
ContentSecurityPolicy,
229+
FeaturePolicy,
229230
ForceSSL,
230231
Streaming,
231232
DataStreaming,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# frozen_string_literal: true
2+
3+
module ActionController #:nodoc:
4+
# HTTP Feature Policy is a web standard for defining a mechanism to
5+
# allow and deny the use of browser features in its own context, and
6+
# in content within any <iframe> elements in the document.
7+
#
8+
# Full details of HTTP Feature Policy specification and guidelines can
9+
# be found at MDN:
10+
#
11+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
12+
#
13+
# Examples of usage:
14+
#
15+
# # Global policy
16+
# Rails.application.config.feature_policy do |f|
17+
# f.camera :none
18+
# f.gyroscope :none
19+
# f.microphone :none
20+
# f.usb :none
21+
# f.fullscreen :self
22+
# f.payment :self, "https://secure-example.com"
23+
# end
24+
#
25+
# # Controller level policy
26+
# class PagesController < ApplicationController
27+
# feature_policy do |p|
28+
# p.geolocation "https://example.com"
29+
# end
30+
# end
31+
module FeaturePolicy
32+
extend ActiveSupport::Concern
33+
34+
module ClassMethods
35+
def feature_policy(**options, &block)
36+
before_action(options) do
37+
if block_given?
38+
policy = request.feature_policy.clone
39+
yield policy
40+
request.feature_policy = policy
41+
end
42+
end
43+
end
44+
end
45+
end
46+
end

actionpack/lib/action_dispatch.rb

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class IllegalStateError < StandardError
4343
eager_autoload do
4444
autoload_under "http" do
4545
autoload :ContentSecurityPolicy
46+
autoload :FeaturePolicy
4647
autoload :Request
4748
autoload :Response
4849
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/core_ext/object/deep_dup"
4+
5+
module ActionDispatch #:nodoc:
6+
class FeaturePolicy
7+
class Middleware
8+
CONTENT_TYPE = "Content-Type"
9+
POLICY = "Feature-Policy"
10+
11+
def initialize(app)
12+
@app = app
13+
end
14+
15+
def call(env)
16+
request = ActionDispatch::Request.new(env)
17+
_, headers, _ = response = @app.call(env)
18+
19+
return response unless html_response?(headers)
20+
return response if policy_present?(headers)
21+
22+
if policy = request.feature_policy
23+
headers[POLICY] = policy.build(request.controller_instance)
24+
end
25+
26+
if policy_empty?(policy)
27+
headers.delete(POLICY)
28+
end
29+
30+
response
31+
end
32+
33+
private
34+
def html_response?(headers)
35+
if content_type = headers[CONTENT_TYPE]
36+
content_type =~ /html/
37+
end
38+
end
39+
40+
def policy_present?(headers)
41+
headers[POLICY]
42+
end
43+
44+
def policy_empty?(policy)
45+
policy.try(:directives) && policy.directives.empty?
46+
end
47+
end
48+
49+
module Request
50+
POLICY = "action_dispatch.feature_policy"
51+
52+
def feature_policy
53+
get_header(POLICY)
54+
end
55+
56+
def feature_policy=(policy)
57+
set_header(POLICY, policy)
58+
end
59+
end
60+
61+
MAPPINGS = {
62+
self: "'self'",
63+
none: "'none'",
64+
}.freeze
65+
66+
# List of available features can be found at
67+
# https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
68+
DIRECTIVES = {
69+
accelerometer: "accelerometer",
70+
ambient_light_sensor: "ambient-light-sensor",
71+
autoplay: "autoplay",
72+
camera: "camera",
73+
encrypted_media: "encrypted-media",
74+
fullscreen: "fullscreen",
75+
geolocation: "geolocation",
76+
gyroscope: "gyroscope",
77+
magnetometer: "magnetometer",
78+
microphone: "microphone",
79+
midi: "midi",
80+
payment: "payment",
81+
picture_in_picture: "picture-in-picture",
82+
speaker: "speaker",
83+
usb: "usb",
84+
vibrate: "vibrate",
85+
vr: "vr",
86+
}.freeze
87+
88+
private_constant :MAPPINGS, :DIRECTIVES
89+
90+
attr_reader :directives
91+
92+
def initialize
93+
@directives = {}
94+
yield self if block_given?
95+
end
96+
97+
def initialize_copy(other)
98+
@directives = other.directives.deep_dup
99+
end
100+
101+
DIRECTIVES.each do |name, directive|
102+
define_method(name) do |*sources|
103+
if sources.first
104+
@directives[directive] = apply_mappings(sources)
105+
else
106+
@directives.delete(directive)
107+
end
108+
end
109+
end
110+
111+
def build(context = nil)
112+
build_directives(context).compact.join("; ")
113+
end
114+
115+
private
116+
def apply_mappings(sources)
117+
sources.map do |source|
118+
case source
119+
when Symbol
120+
apply_mapping(source)
121+
when String, Proc
122+
source
123+
else
124+
raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
125+
end
126+
end
127+
end
128+
129+
def apply_mapping(source)
130+
MAPPINGS.fetch(source) do
131+
raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
132+
end
133+
end
134+
135+
def build_directives(context)
136+
@directives.map do |directive, sources|
137+
if sources.is_a?(Array)
138+
"#{directive} #{build_directive(sources, context).join(' ')}"
139+
elsif sources
140+
directive
141+
else
142+
nil
143+
end
144+
end
145+
end
146+
147+
def build_directive(sources, context)
148+
sources.map { |source| resolve_source(source, context) }
149+
end
150+
151+
def resolve_source(source, context)
152+
case source
153+
when String
154+
source
155+
when Symbol
156+
source.to_s
157+
when Proc
158+
if context.nil?
159+
raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
160+
else
161+
context.instance_exec(&source)
162+
end
163+
else
164+
raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
165+
end
166+
end
167+
end
168+
end

actionpack/lib/action_dispatch/http/request.rb

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class Request
2323
include ActionDispatch::Http::FilterParameters
2424
include ActionDispatch::Http::URL
2525
include ActionDispatch::ContentSecurityPolicy::Request
26+
include ActionDispatch::FeaturePolicy::Request
2627
include Rack::Request::Env
2728

2829
autoload :Session, "action_dispatch/request/session"

0 commit comments

Comments
 (0)