This lack middleware provides some level of security for your webapp,
preventing a CSRF attacks. It has a function csrf-html-tag
which returns
a hidden input
element to embed into a form.
The input stores a special token. Middleware saves this token into the current session and ensures the user sends this token in the following requests. If he doesn’t, a 400 status code will be returned.
Let’s take our yesterday’s app and make it more secure!
First, we need to rewrite our main application to make it render a login
form with CSRF token. Pay attention to how does it call a csrf-html-tag
function at the end.
If you are going to develop an application with a lots of forms, then it is good idea to define a macro which will apply CSRF protection automatically.
POFTHEDAY> (defun main (env)
(let* ((session (getf env :lack.session))
(login (gethash :login session)))
(cond
(login
(list 200 (list :content-type "text/plain")
(list (format nil "Welcome, ~A!"
login))))
(t
(list 200 (list :content-type "text/plain")
(list (format nil "
<form method=\"POST\" action=\"/login\">
<input type=\"text\" name=\"login\"></input>
<input type=\"password\" name=\"password\"></input>
~A
</form>
"
(lack.middleware.csrf:csrf-html-tag session))))))))
All other apps remain the same, we only need to build the whole app
including the csrf
middleware.
This middleware should go after the :session
middleware, because it
depends on it:
POFTHEDAY> (clack:clackup
(lack:builder
:session
:csrf
(:mount "/login" 'login)
(:mount "/logout" 'logout)
'main)
:port 8091)
Hunchentoot server is started.
Listening on 127.0.0.1:8091.
This is how our form is rendered. Note a “hidden” input at the end of the form:
POFTHEDAY> (dex:get "http://localhost:8091/")
"
<form method=\"POST\" action=\"/login\">
<input type=\"text\" name=\"login\"></input>
<input type=\"password\" name=\"password\"></input>
<input type=\"hidden\" name=\"_csrf_token\" value=\"8de1c8a47\">
</form>
If we try to do a POST
request without the token, we’ll receive a 400
error:
POFTHEDAY> (handler-case
(dex:post "http://localhost:8091/login"
:content '(("login" . "bob")
("password" . "$ecret"))
:headers '((:cookie . "lack.session=75bccc")))
(dexador:http-request-failed (c)
(values (dexador:response-status c)
(dexador:response-body c))))
400
"Bad Request: invalid CSRF token"
Using the code we’ll be able to log in:
POFTHEDAY> (dex:post "http://localhost:8091/login"
:content '(("login" . "bob")
("password" . "$ecret")
("_csrf_token" . "8de1c8a47"))
:headers '((:cookie . "lack.session=75bccc")))
"Dear Bob, you welcome!"
200
The middleware also has a few settings.
You can set :session-key
to a value other than _csrf_token
. But this
changes only a token’s key inside the session. Form field’s name remains
the _csrf_token
.
Other option is :one-time
. Set it to true if you want to remove a token
from the session after the first successful POST
, PUT
, DELETE
or PATCH
.
And finally, you can define your own handler for the error page and pass it as “:block-app”. It should be a usual Clack app.