Class: SolidObjects::Web::CsrfProtection
- Inherits:
-
Object
- Object
- SolidObjects::Web::CsrfProtection
- Defined in:
- lib/solid_objects/web/csrf_protection.rb,
sig/generated/lib/solid_objects/web/csrf_protection.rbs
Overview
A state changing request must carry the token of the session that asked for the form. The token a form receives is masked with a fresh one-time pad on every request, so the bytes on the wire differ each time and a compression side channel cannot recover the session token.
Constant Summary collapse
- SAFE_METHODS =
%w[GET HEAD OPTIONS TRACE].freeze
- TOKEN_BYTES =
32- MISSING_SESSION =
<<~MESSAGE SolidObjects::Web needs a Rack session for CSRF protection. Mount it inside the application routes so the Rails session middleware runs first: Rails.application.routes.draw do mount SolidObjects::Web => "/solid_objects" end In a bare Rack application, run a session middleware before it: use Rack::Session::Cookie, secret: ENV.fetch("SESSION_SECRET"), same_site: true run SolidObjects::Web MESSAGE
Instance Method Summary collapse
- #accept?(env) ⇒ Boolean
- #call(env) ⇒ Array[untyped]
- #decode(token) ⇒ String?
- #encode(token) ⇒ String
- #exclusive_or(left, right) ⇒ String
- #forbidden ⇒ Array[untyped]
-
#initialize(app) ⇒ CsrfProtection
constructor
A new instance of CsrfProtection.
- #mask(token) ⇒ String
- #matches?(token, stored) ⇒ Boolean
- #session!(env) ⇒ Hash[untyped, untyped]
- #unmask(masked) ⇒ String
- #valid?(env, given) ⇒ Boolean
Constructor Details
#initialize(app) ⇒ CsrfProtection
Returns a new instance of CsrfProtection.
33 34 35 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 33 def initialize(app) @app = app end |
Instance Method Details
#accept?(env) ⇒ Boolean
50 51 52 53 54 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 50 def accept?(env) return true if SAFE_METHODS.include?(env["REQUEST_METHOD"]) valid?(env, ::Rack::Request.new(env).params["authenticity_token"]) end |
#call(env) ⇒ Array[untyped]
38 39 40 41 42 43 44 45 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 38 def call(env) return forbidden unless accept?(env) session = session!(env) session[:csrf] ||= SecureRandom.base64(TOKEN_BYTES) env[Web::CSRF_TOKEN_KEY] = mask(session[:csrf]) @app.call(env) end |
#decode(token) ⇒ String?
112 113 114 115 116 117 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 112 def decode(token) decoded = token.tr("-_", "+/").unpack1("m0") decoded.is_a?(String) ? decoded : nil rescue ArgumentError nil end |
#encode(token) ⇒ String
107 108 109 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 107 def encode(token) [ token ].pack("m0").tr("+/", "-_") end |
#exclusive_or(left, right) ⇒ String
102 103 104 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 102 def exclusive_or(left, right) left.bytes.zip(right.bytes).map { |first, second| first ^ second.to_i }.pack("c*") end |
#forbidden ⇒ Array[untyped]
125 126 127 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 125 def forbidden [ 403, { "content-type" => "text/plain" }, [ "Forbidden" ] ] end |
#mask(token) ⇒ String
89 90 91 92 93 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 89 def mask(token) decoded = decode(token).to_s pad = SecureRandom.random_bytes(decoded.bytesize) encode(pad + exclusive_or(pad, decoded)) end |
#matches?(token, stored) ⇒ Boolean
78 79 80 81 82 83 84 85 86 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 78 def matches?(token, stored) candidate = case token.bytesize when TOKEN_BYTES then token when TOKEN_BYTES * 2 then unmask(token) else return false end ::Rack::Utils.secure_compare(candidate, decode(stored).to_s) end |
#session!(env) ⇒ Hash[untyped, untyped]
120 121 122 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 120 def session!(env) env["rack.session"] || raise(MISSING_SESSION) end |
#unmask(masked) ⇒ String
96 97 98 99 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 96 def unmask(masked) half = masked.bytesize / 2 exclusive_or(masked[0, half].to_s, masked[half..].to_s) end |
#valid?(env, given) ⇒ Boolean
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/solid_objects/web/csrf_protection.rb', line 57 def valid?(env, given) return false if given.nil? || given.empty? session = session!(env) stored = session[:csrf] return false if stored.nil? token = decode(given) return false unless token # The secret is not rotated here. A page renders one Retry form per # dead letter, and a browser keeps pages open in other tabs, so # spending the secret on the first submission would answer 403 to # every other form already rendered. Single use is not what a CSRF # token provides: it proves the request came from a page this session # was served, and the per-request mask below is what keeps the value # on the wire from repeating. matches?(token, stored) end |