mirror of
https://gitea.invidious.io/iv-org/shard-kemal.git
synced 2024-08-15 00:53:36 +00:00
Implement CSRF protection
This adds a middleware which, when activated, will deny any form submission which does not include a valid `authenticity_token` parameter or `http-x-csrf-token` header with the request. The header and parameter names are identical to the ones supported by Ruby's rack-protection gem for interoperability purposes.
This commit is contained in:
parent
7e49237468
commit
e407d0195c
2 changed files with 117 additions and 0 deletions
73
spec/middleware/csrf_spec.cr
Normal file
73
spec/middleware/csrf_spec.cr
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
require "../spec_helper"
|
||||||
|
|
||||||
|
describe "Kemal::Middleware::CSRF" do
|
||||||
|
|
||||||
|
it "sends GETs to next handler" do
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("GET", "/")
|
||||||
|
io_with_context = create_request_and_return_io(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false)
|
||||||
|
client_response.status_code.should eq 404
|
||||||
|
end
|
||||||
|
|
||||||
|
it "blocks POSTs without the token" do
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("POST", "/")
|
||||||
|
io_with_context = create_request_and_return_io(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io_with_context, decompress: false)
|
||||||
|
client_response.status_code.should eq 403
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows POSTs with the correct token in FORM submit" do
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("POST", "/",
|
||||||
|
body: "authenticity_token=cemal&hasan=lamec",
|
||||||
|
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"})
|
||||||
|
io, context = process_request(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io, decompress: false)
|
||||||
|
client_response.status_code.should eq 403
|
||||||
|
|
||||||
|
current_token = context.session["csrf"]
|
||||||
|
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("POST", "/",
|
||||||
|
body: "authenticity_token=#{current_token}&hasan=lamec",
|
||||||
|
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded",
|
||||||
|
"Set-Cookie" => client_response.headers["Set-Cookie"]})
|
||||||
|
io, context = process_request(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io, decompress: false)
|
||||||
|
client_response.status_code.should eq 404
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allows POSTs with the correct token in HTTP header" do
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("POST", "/",
|
||||||
|
body: "hasan=lamec",
|
||||||
|
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded"})
|
||||||
|
io, context = process_request(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io, decompress: false)
|
||||||
|
client_response.status_code.should eq 403
|
||||||
|
|
||||||
|
current_token = context.session["csrf"]
|
||||||
|
|
||||||
|
handler = Kemal::Middleware::CSRF.new
|
||||||
|
request = HTTP::Request.new("POST", "/",
|
||||||
|
body: "hasan=lamec",
|
||||||
|
headers: HTTP::Headers{"Content-Type" => "application/x-www-form-urlencoded",
|
||||||
|
"Set-Cookie" => client_response.headers["Set-Cookie"],
|
||||||
|
"http-x-csrf-token" => current_token })
|
||||||
|
io, context = process_request(handler, request)
|
||||||
|
client_response = HTTP::Client::Response.from_io(io, decompress: false)
|
||||||
|
client_response.status_code.should eq 404
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def process_request(handler, request)
|
||||||
|
io = MemoryIO.new
|
||||||
|
response = HTTP::Server::Response.new(io)
|
||||||
|
context = HTTP::Server::Context.new(request, response)
|
||||||
|
handler.call(context)
|
||||||
|
response.close
|
||||||
|
io.rewind
|
||||||
|
{io, context}
|
||||||
|
end
|
44
src/kemal/middleware/csrf.cr
Normal file
44
src/kemal/middleware/csrf.cr
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
require "secure_random"
|
||||||
|
require "http"
|
||||||
|
|
||||||
|
module Kemal::Middleware
|
||||||
|
# This middleware adds CSRF protection to your application.
|
||||||
|
#
|
||||||
|
# Returns 403 "Forbidden" unless the current CSRF token is submitted
|
||||||
|
# with any non-GET/HEAD request.
|
||||||
|
#
|
||||||
|
# Without CSRF protection, your app is vulnerable to replay attacks
|
||||||
|
# where an attacker can re-submit a form.
|
||||||
|
#
|
||||||
|
class CSRF < HTTP::Handler
|
||||||
|
HEADER = "HTTP_X_CSRF_TOKEN"
|
||||||
|
ALLOWED_METHODS = %w[GET HEAD OPTIONS TRACE]
|
||||||
|
PARAMETER_NAME = "authenticity_token"
|
||||||
|
|
||||||
|
def call(context)
|
||||||
|
return call_next(context) if ALLOWED_METHODS.includes?(context.request.method)
|
||||||
|
|
||||||
|
req = context.request
|
||||||
|
current_token = context.session["csrf"]? || begin
|
||||||
|
context.session["csrf"] = SecureRandom.hex(16)
|
||||||
|
end
|
||||||
|
submitted = if req.headers[HEADER]?
|
||||||
|
req.headers[HEADER]
|
||||||
|
elsif context.params.body[PARAMETER_NAME]?
|
||||||
|
context.params.body[PARAMETER_NAME]
|
||||||
|
else
|
||||||
|
"nothing"
|
||||||
|
end
|
||||||
|
|
||||||
|
if current_token == submitted
|
||||||
|
# reset the token so it can't be used again
|
||||||
|
context.session["csrf"] = SecureRandom.hex(16)
|
||||||
|
return call_next(context)
|
||||||
|
else
|
||||||
|
context.response.status_code = 403
|
||||||
|
context.response.print "Forbidden"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue