diff options
Diffstat (limited to 'actionpack/lib')
| -rw-r--r-- | actionpack/lib/action_controller.rb | 1 | ||||
| -rw-r--r-- | actionpack/lib/action_controller/base.rb | 1 | ||||
| -rw-r--r-- | actionpack/lib/action_controller/metal/feature_policy.rb | 46 | ||||
| -rw-r--r-- | actionpack/lib/action_dispatch.rb | 1 | ||||
| -rw-r--r-- | actionpack/lib/action_dispatch/http/feature_policy.rb | 168 | ||||
| -rw-r--r-- | actionpack/lib/action_dispatch/http/request.rb | 1 | 
6 files changed, 218 insertions, 0 deletions
| diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index 29d61c3ceb..dfa49fcc36 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -28,6 +28,7 @@ module ActionController      autoload :DefaultHeaders      autoload :EtagWithTemplateDigest      autoload :EtagWithFlash +    autoload :FeaturePolicy      autoload :Flash      autoload :ForceSSL      autoload :Head diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 2e565d5d44..63c138af55 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -226,6 +226,7 @@ module ActionController        FormBuilder,        RequestForgeryProtection,        ContentSecurityPolicy, +      FeaturePolicy,        ForceSSL,        Streaming,        DataStreaming, diff --git a/actionpack/lib/action_controller/metal/feature_policy.rb b/actionpack/lib/action_controller/metal/feature_policy.rb new file mode 100644 index 0000000000..eecca20dda --- /dev/null +++ b/actionpack/lib/action_controller/metal/feature_policy.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module ActionController #:nodoc: +  # HTTP Feature Policy is a web standard for defining a mechanism to +  # allow and deny the use of browser features in its own context, and +  # in content within any <iframe> elements in the document. +  # +  # Full details of HTTP Feature Policy specification and guidelines can +  # be found at MDN: +  # +  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy +  # +  # Examples of usage: +  # +  #   # Global policy +  #   Rails.application.config.feature_policy do |f| +  #     f.camera      :none +  #     f.gyroscope   :none +  #     f.microphone  :none +  #     f.usb         :none +  #     f.fullscreen  :self +  #     f.payment     :self, "https://secure-example.com" +  #   end +  # +  #   # Controller level policy +  #   class PagesController < ApplicationController +  #     feature_policy do |p| +  #       p.geolocation "https://example.com" +  #     end +  #   end +  module FeaturePolicy +    extend ActiveSupport::Concern + +    module ClassMethods +      def feature_policy(**options, &block) +        before_action(options) do +          if block_given? +            policy = request.feature_policy.clone +            yield policy +            request.feature_policy = policy +          end +        end +      end +    end +  end +end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 6a4ba9af4a..67d303a368 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -43,6 +43,7 @@ module ActionDispatch    eager_autoload do      autoload_under "http" do        autoload :ContentSecurityPolicy +      autoload :FeaturePolicy        autoload :Request        autoload :Response      end diff --git a/actionpack/lib/action_dispatch/http/feature_policy.rb b/actionpack/lib/action_dispatch/http/feature_policy.rb new file mode 100644 index 0000000000..592b6e4393 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/feature_policy.rb @@ -0,0 +1,168 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/deep_dup" + +module ActionDispatch #:nodoc: +  class FeaturePolicy +    class Middleware +      CONTENT_TYPE = "Content-Type" +      POLICY       = "Feature-Policy" + +      def initialize(app) +        @app = app +      end + +      def call(env) +        request = ActionDispatch::Request.new(env) +        _, headers, _ = response = @app.call(env) + +        return response unless html_response?(headers) +        return response if policy_present?(headers) + +        if policy = request.feature_policy +          headers[POLICY] = policy.build(request.controller_instance) +        end + +        if policy_empty?(policy) +          headers.delete(POLICY) +        end + +        response +      end + +      private +        def html_response?(headers) +          if content_type = headers[CONTENT_TYPE] +            content_type =~ /html/ +          end +        end + +        def policy_present?(headers) +          headers[POLICY] +        end + +        def policy_empty?(policy) +          policy.try(:directives) && policy.directives.empty? +        end +    end + +    module Request +      POLICY = "action_dispatch.feature_policy" + +      def feature_policy +        get_header(POLICY) +      end + +      def feature_policy=(policy) +        set_header(POLICY, policy) +      end +    end + +    MAPPINGS = { +      self: "'self'", +      none: "'none'", +    }.freeze + +    # List of available features can be found at +    # https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features +    DIRECTIVES = { +      accelerometer:        "accelerometer", +      ambient_light_sensor: "ambient-light-sensor", +      autoplay:             "autoplay", +      camera:               "camera", +      encrypted_media:      "encrypted-media", +      fullscreen:           "fullscreen", +      geolocation:          "geolocation", +      gyroscope:            "gyroscope", +      magnetometer:         "magnetometer", +      microphone:           "microphone", +      midi:                 "midi", +      payment:              "payment", +      picture_in_picture:   "picture-in-picture", +      speaker:              "speaker", +      usb:                  "usb", +      vibrate:              "vibrate", +      vr:                   "vr", +    }.freeze + +    private_constant :MAPPINGS, :DIRECTIVES + +    attr_reader :directives + +    def initialize +      @directives = {} +      yield self if block_given? +    end + +    def initialize_copy(other) +      @directives = other.directives.deep_dup +    end + +    DIRECTIVES.each do |name, directive| +      define_method(name) do |*sources| +        if sources.first +          @directives[directive] = apply_mappings(sources) +        else +          @directives.delete(directive) +        end +      end +    end + +    def build(context = nil) +      build_directives(context).compact.join("; ") +    end + +    private +      def apply_mappings(sources) +        sources.map do |source| +          case source +          when Symbol +            apply_mapping(source) +          when String, Proc +            source +          else +            raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}" +          end +        end +      end + +      def apply_mapping(source) +        MAPPINGS.fetch(source) do +          raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}" +        end +      end + +      def build_directives(context) +        @directives.map do |directive, sources| +          if sources.is_a?(Array) +            "#{directive} #{build_directive(sources, context).join(' ')}" +          elsif sources +            directive +          else +            nil +          end +        end +      end + +      def build_directive(sources, context) +        sources.map { |source| resolve_source(source, context) } +      end + +      def resolve_source(source, context) +        case source +        when String +          source +        when Symbol +          source.to_s +        when Proc +          if context.nil? +            raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}" +          else +            context.instance_exec(&source) +          end +        else +          raise RuntimeError, "Unexpected feature policy source: #{source.inspect}" +        end +      end +  end +end diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 44f23940d3..4ac7c5c2bd 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -23,6 +23,7 @@ module ActionDispatch      include ActionDispatch::Http::FilterParameters      include ActionDispatch::Http::URL      include ActionDispatch::ContentSecurityPolicy::Request +    include ActionDispatch::FeaturePolicy::Request      include Rack::Request::Env      autoload :Session, "action_dispatch/request/session" | 
