lib/rack/protection/cookie_tossing.rb



# frozen_string_literal: true

require 'rack/protection'
require 'pathname'

module Rack
  module Protection
    ##
    # Prevented attack::   Cookie Tossing
    # Supported browsers:: all
    # More infos::         https://github.com/blog/1466-yummy-cookies-across-domains
    #
    # Does not accept HTTP requests if the HTTP_COOKIE header contains more than one
    # session cookie. This does not protect against a cookie overflow attack.
    #
    # Options:
    #
    # session_key:: The name of the session cookie (default: 'rack.session')
    class CookieTossing < Base
      default_reaction :deny

      def call(env)
        status, headers, body = super
        response = Rack::Response.new(body, status, headers)
        request = Rack::Request.new(env)
        remove_bad_cookies(request, response)
        response.finish
      end

      def accepts?(env)
        cookie_header = env['HTTP_COOKIE']
        cookies = Rack::Utils.parse_query(cookie_header, ';,') { |s| s }
        cookies.each do |k, v|
          if (k == session_key && Array(v).size > 1) ||
             (k != session_key && Rack::Utils.unescape(k) == session_key)
            bad_cookies << k
          end
        end
        bad_cookies.empty?
      end

      def remove_bad_cookies(request, response)
        return if bad_cookies.empty?

        paths = cookie_paths(request.path)
        bad_cookies.each do |name|
          paths.each { |path| response.set_cookie name, empty_cookie(request.host, path) }
        end
      end

      def redirect(env)
        request = Request.new(env)
        warn env, "attack prevented by #{self.class}"
        [302, { 'Content-Type' => 'text/html', 'Location' => request.path }, []]
      end

      def bad_cookies
        @bad_cookies ||= []
      end

      def cookie_paths(path)
        path = '/' if path.to_s.empty?
        paths = []
        Pathname.new(path).descend { |p| paths << p.to_s }
        paths
      end

      def empty_cookie(host, path)
        { value: '', domain: host, path: path, expires: Time.at(0) }
      end

      def session_key
        @session_key ||= options[:session_key]
      end
    end
  end
end