Created
August 18, 2019 15:37
-
-
Save julik/a2cf435bc60c47b67f0a364ab8469560 to your computer and use it in GitHub Desktop.
Minimum viable reuse of the Rails CSRF token in Rack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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