diff --git a/README.md b/README.md index e3a14ae..b05d31e 100644 --- a/README.md +++ b/README.md @@ -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 + +``` + +Inject token into the template: +```erlang +Template:render([{csrf_token, CsrfToken}]), +``` + cowboy_common_handler -------------- diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl new file mode 100644 index 0000000..3f9a705 --- /dev/null +++ b/src/cowboy_csrf.erl @@ -0,0 +1,85 @@ +%% +%% @doc Simple CSRF prevention +%% +%% NB: Apply after cowboy_session middleware +%% + +-module(cowboy_csrf). +-author('rambocoder '). + +-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.