Skip to content

Instantly share code, notes, and snippets.

@julik
Created August 18, 2019 15:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save julik/a2cf435bc60c47b67f0a364ab8469560 to your computer and use it in GitHub Desktop.
Save julik/a2cf435bc60c47b67f0a364ab8469560 to your computer and use it in GitHub Desktop.
Minimum viable reuse of the Rails CSRF token in Rack
# Allows a Rack application to reuse the Rails-provided CSRF
# protection, with the same guarantees and the same token.
# Implements token unmasking and other facilties.
class Middleware::CSRFAdapter
AUTHENTICITY_TOKEN_LENGTH = 32
class InvalidOrMissingToken < StandardError
def http_status_code
403
end
end
def initialize(app)
@app = app
end
def xor_byte_strings(s1, s2)
s2_bytes = s2.bytes
s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
s2_bytes.pack("C*")
end
def unmask_token(masked_token)
# Split the token into the one-time pad and the encrypted
# value and decrypt it.
one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
xor_byte_strings(one_time_pad, encrypted_csrf_token)
end
def call(env)
# Check whether we have a _csrf_token in the
# session. If we do not, we should refuse the
# request (the user MUST have loaded the HTML
# shell which sets the CSRF meta tag prior to
# performing _any_ API requests, always). The _csrf_token
# gets written into the session by Rails as a side
# effect of rendering the csrf_meta into the rendered page (our shell)
# Verify the token
req = Rack::Request.new(env)
if req.options? || req.head? || req.get?
return @app.call(env) # idempotent request
end
if token_present_and_valid?(env)
@app.call(env)
else
raise InvalidOrMissingToken
end
end
def token_present_and_valid?(env)
session = env['rack.session']
token_from_session = Base64.strict_decode64(session['_csrf_token'])
encoded_masked_token_from_request = env['HTTP_X_CSRF_TOKEN']
masked_token_from_request = Base64.strict_decode64(encoded_masked_token_from_request)
token_from_request = unmask_token(masked_token_from_request)
Rack::Utils.secure_compare(token_from_session, token_from_request)
rescue => e
false
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment