Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vendor rack-protection/authenticity_token middleware #4588

Merged
merged 3 commits into from Jun 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
3 changes: 0 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
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
254 changes: 254 additions & 0 deletions lib/sidekiq/web/authenticity_token.rb
@@ -0,0 +1,254 @@
# this file came from the sinatra/rack-protection project
#
# The MIT License (MIT)
#
# Copyright (c) 2011-2017 Konstantin Haase
# Copyright (c) 2015-2017 Zachary Scott
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# 'Software'), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require "securerandom"
require "base64"
require "rack/request"

module Sidekiq
class Web
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 || app.call(env)
end

def react(env)
result = send(options[:reaction], env)
result if (Array === result) && (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] && 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

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)
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]&.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