# 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