Skip to content

CSRF with setting the buffer back after reading _csrf token #4

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,50 @@ Req4 = cowboy_session:drop(Req3).

Consider writing other implementations :)

cowboy_csrf
--------------

Provide cross site request forgery (CSRF) protection.

Insert `cowboy_csrf` middleware after `cowboy_session`:

```erlang
{middlewares, [
...
cowboy_session,
...
cowboy_csrf, % requires cowboy_session
...
]}
```

Generate or retrieve CSRF from session:
```erlang
{CsrfToken, Req3} = case proplists:get_value(csrf_token, Session1, undefined) of
undefined ->
% did not find csrf_token in session
% generate a new token and add it to the session
NewToken = base64:encode(crypto:strong_rand_bytes(32)),
Session2 = Session1 ++ [{csrf_token, NewToken}],
Req2 = cowboy_req:set_resp_header(<<"x-csrf-token">>, NewToken, Req1),
Req2a = cowboy_session:set(Session2, Req2),
{NewToken, Req2a};
ExistingCsrfToken ->
% found an existing csrf_token in session
{ExistingCsrfToken, Req1}
end
```

Reffer to the token in ErlyDTL template's form:
```html
<input type="hidden" name="_csrf" value="{{csrf_token}}"/>
```

Inject token into the template:
```erlang
Template:render([{csrf_token, CsrfToken}]),
```

cowboy_common_handler
--------------

Expand Down
85 changes: 85 additions & 0 deletions src/cowboy_csrf.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
%%
%% @doc Simple CSRF prevention
%%
%% NB: Apply after cowboy_session middleware
%%

-module(cowboy_csrf).
-author('rambocoder <[email protected]>').

-behaviour(cowboy_middleware).
-export([execute/2]).

%%
%% @doc Middleware verifying CSRF toaken in request.
%%

execute(Req0, Env0) ->
case cowboy_req:method(Req0) of
{<<"GET">>, Req1} -> {ok, Req1, Env0};
{<<"HEAD">>, Req1} -> {ok, Req1, Env0};
{<<"OPTIONS">>, Req1} -> {ok, Req1, Env0};
{_, Req1} ->
% check if CSRF token is in body, query string, header
case csrf_from_body(Req1) of
{undefined, Req2} ->
{ok, Req3} = cowboy_req:reply(403, [], <<"Body does not contain CSRF token">>, Req2),
{error, 403, Req3};
{error, _Reason} -> {error, 500, Req1};
{CSRFTokenValue, Req2} -> check_session_first(CSRFTokenValue, Req2, Env0)
end
end.

check_session_first(CSRFTokenValue, Req2, Env0) ->
case cowboy_session:get(Req2) of
{undefined, Req3} ->
Req4 = cowboy_session:set([], Req3),
found_session(CSRFTokenValue, [], Req4, Env0);
{Session, Req3} ->
found_session(CSRFTokenValue, Session, Req3, Env0)
end.

found_session(CSRFTokenValue, Session, Req3, Env0) ->
case proplists:get_value(csrf_token, Session, undefined) of
undefined ->
% no csrf_token in session found
% let's generate a new one and add it
NewToken = base64:encode(crypto:strong_rand_bytes(32)),
Session2 = Session ++ {csrf_token, NewToken},
cowboy_session:set(Session2, Req3),
Req4 = cowboy_req:set_resp_body(<<"Session was invalid. It did not contain CSRF token.">>, Req3),
{error, 403, Req4};
CSRFTokenValue ->
{ok, Req3, Env0};
SessionCSRFTokenValue ->
Req4 = cowboy_req:set_resp_body(<<"Invalid CSRF token">>, Req3),
{error, 403, Req4}
end.

csrf_from_body(Req0) ->
% check in the body
case cowboy_req:body(Req0) of
{error, Reason} -> {error, Reason};
{ok, Buffer, Req1} ->
BodyQs = cowboy_http:x_www_form_urlencoded(Buffer),
Req2 = cowboy_req:set([{buffer, Buffer}], Req1),
Req3 = cowboy_req:set([{body_state, waiting}], Req2),
case proplists:get_value(<<"_csrf">>, BodyQs, undefined) of
undefined -> csrf_from_querystring(Req3);
TokenValue -> {TokenValue, Req3}
end
end.

csrf_from_querystring(Req0) ->
% check in the query string
case cowboy_req:qs_val(<<"_csrf">>, Req0, undefined) of
{undefined, Req1} -> csrf_from_header(Req1);
{TokenValue, Req1} -> {TokenValue, Req1}
end.

csrf_from_header(Req0) ->
% check in the header
case cowboy_req:header(<<"x-csrf-token">>, Req0, undefined) of
{undefined, Req1} -> {undefined, Req1};
{TokenValue, Req1} -> {TokenValue, Req1}
end.