Skip to content

Commit

Permalink
[COVID-19] Vendor rack-protection/authenticity_token middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
seuros committed Jun 3, 2020
1 parent 07cc382 commit 9591b9c
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 7 deletions.
8 changes: 8 additions & 0 deletions .circleci/config.yml
Expand Up @@ -10,6 +10,10 @@ references:
- v1-dependencies-{{ checksum "Gemfile.lock" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
update_bundle: &update_bundle
run:
name: Install bundler 2
command: gem install bundler
bundle: &bundle
run:
name: install dependencies
Expand All @@ -27,6 +31,7 @@ jobs:
- image: circleci/redis:4.0
steps:
- checkout
- <<: *update_bundle
- <<: *restore
- <<: *bundle
- <<: *save
Expand All @@ -37,6 +42,7 @@ jobs:
- image: circleci/redis:5.0
steps:
- checkout
- <<: *update_bundle
- <<: *restore
- <<: *bundle
- <<: *save
Expand All @@ -47,6 +53,7 @@ jobs:
- image: circleci/redis:6.0
steps:
- checkout
- <<: *update_bundle
- <<: *restore
- <<: *bundle
- <<: *save
Expand All @@ -57,6 +64,7 @@ jobs:
- image: circleci/redis:4.0
steps:
- checkout
- <<: *update_bundle
- <<: *restore
- <<: *bundle
- <<: *save
Expand Down
1 change: 1 addition & 0 deletions Changes.md
Expand Up @@ -9,6 +9,7 @@ HEAD
- Ensure `Rack::ContentLength` is loaded as middleware for correct Web UI responses [#4541]
- Avoid exception dumping SSL store in Redis connection logging [#4532]
- Better error messages in Sidekiq::Client [#4549]
- Vendor rack-protection/authenticity_token middleware [#4588]

6.0.7
---------
Expand Down
6 changes: 3 additions & 3 deletions Gemfile.lock
Expand Up @@ -4,7 +4,6 @@ PATH
sidekiq (6.0.7)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)

GEM
Expand Down Expand Up @@ -117,8 +116,6 @@ GEM
byebug (~> 11.0)
pry (~> 0.10)
rack (2.2.2)
rack-protection (2.0.8.1)
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (6.0.3.1)
Expand Down Expand Up @@ -211,3 +208,6 @@ DEPENDENCIES
sqlite3
standard
toxiproxy

BUNDLED WITH
2.1.4
6 changes: 3 additions & 3 deletions lib/sidekiq/web.rb
Expand Up @@ -10,8 +10,8 @@
require "sidekiq/web/router"
require "sidekiq/web/action"
require "sidekiq/web/application"
require "sidekiq/web/authenticity_token"

require "rack/protection"
require "rack/content_length"

require "rack/builder"
Expand Down Expand Up @@ -155,8 +155,8 @@ def using?(middleware)
def build_sessions
middlewares = self.middlewares

unless using?(::Rack::Protection) || ENV["RACK_ENV"] == "test"
middlewares.unshift [[::Rack::Protection, {use: :authenticity_token}], nil]
unless using?(AuthenticityToken) || ENV["RACK_ENV"] == "test"
middlewares.unshift [[AuthenticityToken], nil]
end

s = sessions
Expand Down
231 changes: 231 additions & 0 deletions lib/sidekiq/web/authenticity_token.rb
@@ -0,0 +1,231 @@
require 'securerandom'
require 'base64'
require 'rack/request'

module Sidekiq
class Web
## Shamelessly copied from Rack-protection gem
class AuthenticityToken
DEFAULT_OPTIONS = {
:reaction => :default_reaction, :logging => true,
:message => 'Forbidden', :encryptor => Digest::SHA1,
:session_key => 'rack.session', :status => 403,
:allow_empty_referrer => true,
:report_key => "protection.failed",
:html_types => %w[text/html application/xhtml text/xml application/xml]
}

attr_reader :app, :options

def self.default_options(options)
define_method(:default_options) { DEFAULT_OPTIONS.merge(options) }
end

def self.default_reaction(reaction)
alias_method(:default_reaction, reaction)
end

def default_options
DEFAULT_OPTIONS
end

def initialize(app, options = {})
@app, @options = app, default_options.merge(options)
end

def safe?(env)
%w[GET HEAD OPTIONS TRACE].include? env['REQUEST_METHOD']
end

def call(env)
unless accepts? env
instrument env
result = react env
end
result or app.call(env)
end

def react(env)
result = send(options[:reaction], env)
result if Array === result and result.size == 3
end

def warn(env, message)
return unless options[:logging]
l = options[:logger] || env['rack.logger'] || ::Logger.new(env['rack.errors'])
l.warn(message)
end

def instrument(env)
return unless i = options[:instrumenter]
env['rack.protection.attack'] = self.class.name.split('::').last.downcase
i.instrument('rack.protection', env)
end

def deny(env)
warn env, "attack prevented by #{self.class}"
[options[:status], {'Content-Type' => 'text/plain'}, [options[:message]]]
end

def report(env)
warn env, "attack reported by #{self.class}"
env[options[:report_key]] = true
end

def session?(env)
env.include? options[:session_key]
end

def session(env)
return env[options[:session_key]] if session? env
fail "you need to set up a session middleware *before* #{self.class}"
end

def drop_session(env)
session(env).clear if session? env
end

def referrer(env)
ref = env['HTTP_REFERER'].to_s
return if !options[:allow_empty_referrer] and ref.empty?
URI.parse(ref).host || Rack::Request.new(env).host
rescue URI::InvalidURIError
end

def origin(env)
env['HTTP_ORIGIN'] || env['HTTP_X_ORIGIN']
end

def random_string(secure = defined? SecureRandom)
secure ? SecureRandom.hex(16) : "%032x" % rand(2**128-1)
rescue NotImplementedError
random_string false
end

def encrypt(value)
options[:encryptor].hexdigest value.to_s
end

def secure_compare(a, b)
Rack::Utils.secure_compare(a.to_s, b.to_s)
end

alias default_reaction deny

def html?(headers)
return false unless header = headers.detect { |k,v| k.downcase == 'content-type' }
options[:html_types].include? header.last[/^\w+\/\w+/]
end

TOKEN_LENGTH = 32

default_options :authenticity_param => 'authenticity_token',
:allow_if => nil

def self.token(session)
self.new(nil).mask_authenticity_token(session)
end

def self.random_token
SecureRandom.base64(TOKEN_LENGTH)
end

def accepts?(env)
session = session env
set_token(session)

safe?(env) ||
valid_token?(session, env['HTTP_X_CSRF_TOKEN']) ||
valid_token?(session, Rack::Request.new(env).params[options[:authenticity_param]]) ||
( options[:allow_if] && options[:allow_if].call(env) )
end

def mask_authenticity_token(session)
token = set_token(session)
mask_token(token)
end

private

def set_token(session)
session[:csrf] ||= self.class.random_token
end

# Checks the client's masked token to see if it matches the
# session token.
def valid_token?(session, token)
return false if token.nil? || token.empty?

begin
token = decode_token(token)
rescue ArgumentError # encoded_masked_token is invalid Base64
return false
end

# See if it's actually a masked token or not. We should be able
# to handle any unmasked tokens that we've issued without error.

if unmasked_token?(token)
compare_with_real_token token, session

elsif masked_token?(token)
token = unmask_token(token)

compare_with_real_token token, session

else
false # Token is malformed
end
end

# Creates a masked version of the authenticity token that varies
# on each request. The masking is used to mitigate SSL attacks
# like BREACH.
def mask_token(token)
token = decode_token(token)
one_time_pad = SecureRandom.random_bytes(token.length)
encrypted_token = xor_byte_strings(one_time_pad, token)
masked_token = one_time_pad + encrypted_token
encode_token(masked_token)
end

# Essentially the inverse of +mask_token+.
def unmask_token(masked_token)
# Split the token into the one-time pad and the encrypted
# value and decrypt it
token_length = masked_token.length / 2
one_time_pad = masked_token[0...token_length]
encrypted_token = masked_token[token_length..-1]
xor_byte_strings(one_time_pad, encrypted_token)
end

def unmasked_token?(token)
token.length == TOKEN_LENGTH
end

def masked_token?(token)
token.length == TOKEN_LENGTH * 2
end

def compare_with_real_token(token, session)
secure_compare(token, real_token(session))
end

def real_token(session)
decode_token(session[:csrf])
end

def encode_token(token)
Base64.strict_encode64(token)
end

def decode_token(token)
Base64.strict_decode64(token)
end

def xor_byte_strings(s1, s2)
s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
end
end
end
end
1 change: 0 additions & 1 deletion sidekiq.gemspec
Expand Up @@ -17,5 +17,4 @@ Gem::Specification.new do |gem|
gem.add_dependency "redis", ">= 4.1.0"
gem.add_dependency "connection_pool", ">= 2.2.2"
gem.add_dependency "rack", "~> 2.0"
gem.add_dependency "rack-protection", ">= 2.0.0"
end

0 comments on commit 9591b9c

Please sign in to comment.