From b26980d292ac42aadfe9921a961436e28cdbb693 Mon Sep 17 00:00:00 2001 From: rambocoder Date: Mon, 15 Apr 2013 16:30:13 -0400 Subject: [PATCH 1/6] CSRF middleware that is pointless because it makes cowboy_req:body_qs/1 empty for subsequent calls. --- src/cowboy_csrf.erl | 67 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/cowboy_csrf.erl diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl new file mode 100644 index 0000000..7551df9 --- /dev/null +++ b/src/cowboy_csrf.erl @@ -0,0 +1,67 @@ +%% +%% @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, [], "Invalid CSRF Token.", Req2), + {error, 403, Req3}; + {error, _Reason} -> {error, 500, Req1}; + {TokenValue, Req2} -> + {Session, Req3} = cowboy_session:get(Req2), + case proplists:get_value(csrf_token, Session, undefined) of + TokenValue -> {ok, Req3, Env0}; + _ -> + % 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), + {error, 403, Req3} + end + end + end. + +csrf_from_body(Req0) -> + % check in the body + case cowboy_req:body_qs(Req0) of + {error, Reason} -> {error, Reason}; + {ok, BodyQs, Req1} -> + case proplists:get_value(<<"_csrf">>, BodyQs, undefined) of + undefined -> csrf_from_querystring(Req1); + TokenValue -> {TokenValue, Req1} + 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. \ No newline at end of file From 88e5f7c26789028d7618498e5351ff534a59c67b Mon Sep 17 00:00:00 2001 From: rambocoder Date: Fri, 14 Mar 2014 13:49:58 -0400 Subject: [PATCH 2/6] Setting the cowboy_req buffer after reading _csrf token --- src/cowboy_csrf.erl | 60 ++++++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl index 7551df9..b8ca4c8 100644 --- a/src/cowboy_csrf.erl +++ b/src/cowboy_csrf.erl @@ -23,32 +23,58 @@ execute(Req0, Env0) -> % check if CSRF token is in body, query string, header case csrf_from_body(Req1) of {undefined, Req2} -> - {ok, Req3} = cowboy_req:reply(403, [], "Invalid CSRF Token.", Req2), + {ok, Req3} = cowboy_req:reply(403, [], "Body does not contain CSRF Token.", Req2), {error, 403, Req3}; {error, _Reason} -> {error, 500, Req1}; - {TokenValue, Req2} -> - {Session, Req3} = cowboy_session:get(Req2), - case proplists:get_value(csrf_token, Session, undefined) of - TokenValue -> {ok, Req3, Env0}; - _ -> - % 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), - {error, 403, Req3} - end + {CSRFTokenValue, Req2} -> check_session_first(CSRFTokenValue, Req2, Env0) end end. +check_session_first(CSRFTokenValue, Req2, Env0) -> + case cowboy_session:get(Req2) of + {undefined, Req3} -> + io:format("No session in current request. Let's create a new one.~n",[]), + 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) -> + io:format("Found session~n",[]), + 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", Req3), + {error, 403, Req4}; + CSRFTokenValue -> + io:format("Session CSRF matches body CSRF ~n",[]), + {ok, Req3, Env0}; + SessionCSRFTokenValue -> + Reason = io_lib:format("Session CSRF ~p did not match body CSRF ~p",[SessionCSRFTokenValue, CSRFTokenValue]), + io:format("~p~n", [Reason]), + Req4 = cowboy_req:set_resp_body(Reason, Req3), + {error, 403, Req4} + end. + + csrf_from_body(Req0) -> % check in the body - case cowboy_req:body_qs(Req0) of + case cowboy_req:body(Req0) of {error, Reason} -> {error, Reason}; - {ok, BodyQs, Req1} -> + {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(Req1); - TokenValue -> {TokenValue, Req1} + undefined -> + io:format("No _csrf in body, check the querystring~n", []), + csrf_from_querystring(Req3); + TokenValue -> {TokenValue, Req3} end end. From 62cdb5a9aa5a23dee98c6732a6865848553b8b0e Mon Sep 17 00:00:00 2001 From: rambocoder Date: Fri, 4 Apr 2014 13:40:48 -0400 Subject: [PATCH 3/6] Remove io:format and converted chars() into binary() --- src/cowboy_csrf.erl | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl index b8ca4c8..d446b7f 100644 --- a/src/cowboy_csrf.erl +++ b/src/cowboy_csrf.erl @@ -23,7 +23,7 @@ execute(Req0, Env0) -> % 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), + {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) @@ -33,7 +33,6 @@ execute(Req0, Env0) -> check_session_first(CSRFTokenValue, Req2, Env0) -> case cowboy_session:get(Req2) of {undefined, Req3} -> - io:format("No session in current request. Let's create a new one.~n",[]), Req4 = cowboy_session:set([], Req3), found_session(CSRFTokenValue, [], Req4, Env0); {Session, Req3} -> @@ -41,7 +40,6 @@ check_session_first(CSRFTokenValue, Req2, Env0) -> end. found_session(CSRFTokenValue, Session, Req3, Env0) -> - io:format("Found session~n",[]), case proplists:get_value(csrf_token, Session, undefined) of undefined -> % no csrf_token in session found @@ -49,14 +47,12 @@ found_session(CSRFTokenValue, Session, Req3, Env0) -> 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", Req3), + Req4 = cowboy_req:set_resp_body(<<"Session was invalid. It did not contain CSRF token.">>, Req3), {error, 403, Req4}; CSRFTokenValue -> - io:format("Session CSRF matches body CSRF ~n",[]), {ok, Req3, Env0}; SessionCSRFTokenValue -> - Reason = io_lib:format("Session CSRF ~p did not match body CSRF ~p",[SessionCSRFTokenValue, CSRFTokenValue]), - io:format("~p~n", [Reason]), + Reason = io_lib:format(<<"Session CSRF token: ~p did not match body CSRF token: ~p">>,[SessionCSRFTokenValue, CSRFTokenValue]), Req4 = cowboy_req:set_resp_body(Reason, Req3), {error, 403, Req4} end. @@ -71,9 +67,7 @@ csrf_from_body(Req0) -> Req2 = cowboy_req:set([{buffer, Buffer}], Req1), Req3 = cowboy_req:set([{body_state, waiting}], Req2), case proplists:get_value(<<"_csrf">>, BodyQs, undefined) of - undefined -> - io:format("No _csrf in body, check the querystring~n", []), - csrf_from_querystring(Req3); + undefined -> csrf_from_querystring(Req3); TokenValue -> {TokenValue, Req3} end end. @@ -90,4 +84,4 @@ csrf_from_header(Req0) -> case cowboy_req:header(<<"x-csrf-token">>, Req0, undefined) of {undefined, Req1} -> {undefined, Req1}; {TokenValue, Req1} -> {TokenValue, Req1} - end. \ No newline at end of file + end. From abc3e820fd6ed651fe38fb76ac8b0dbda6f60d38 Mon Sep 17 00:00:00 2001 From: rambocoder Date: Fri, 4 Apr 2014 14:10:00 -0400 Subject: [PATCH 4/6] CSRF documentation --- README.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) 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 -------------- From 40072e286f047570fe825fd51d52267bcb5136b4 Mon Sep 17 00:00:00 2001 From: rambocoder Date: Fri, 4 Apr 2014 14:43:00 -0400 Subject: [PATCH 5/6] Reason for CSRF token mismatch concat using IO list --- src/cowboy_csrf.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl index d446b7f..724e9e5 100644 --- a/src/cowboy_csrf.erl +++ b/src/cowboy_csrf.erl @@ -52,7 +52,7 @@ found_session(CSRFTokenValue, Session, Req3, Env0) -> CSRFTokenValue -> {ok, Req3, Env0}; SessionCSRFTokenValue -> - Reason = io_lib:format(<<"Session CSRF token: ~p did not match body CSRF token: ~p">>,[SessionCSRFTokenValue, CSRFTokenValue]), + Reason = [<<"Session CSRF token: ">>, SessionCSRFTokenValue, <<" did not match body CSRF token: ">>, CSRFTokenValue], Req4 = cowboy_req:set_resp_body(Reason, Req3), {error, 403, Req4} end. From 25a1979603b2adcb28dab74ebcde35d432a324cf Mon Sep 17 00:00:00 2001 From: rambocoder Date: Thu, 29 May 2014 10:23:27 -0400 Subject: [PATCH 6/6] Removed exposing _csrf token over the wire Removed use of io_lib --- src/cowboy_csrf.erl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cowboy_csrf.erl b/src/cowboy_csrf.erl index 724e9e5..3f9a705 100644 --- a/src/cowboy_csrf.erl +++ b/src/cowboy_csrf.erl @@ -23,7 +23,7 @@ execute(Req0, Env0) -> % 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), + {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) @@ -52,12 +52,10 @@ found_session(CSRFTokenValue, Session, Req3, Env0) -> CSRFTokenValue -> {ok, Req3, Env0}; SessionCSRFTokenValue -> - Reason = [<<"Session CSRF token: ">>, SessionCSRFTokenValue, <<" did not match body CSRF token: ">>, CSRFTokenValue], - Req4 = cowboy_req:set_resp_body(Reason, Req3), + 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