From 03dac1486d72d9d84a3cb99d2040c78b25853257 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 5 Oct 2019 11:23:57 +0200 Subject: Add cowboy_req:filter_cookies/2 --- doc/src/manual/cowboy_req.asciidoc | 1 + doc/src/manual/cowboy_req.filter_cookies.asciidoc | 70 +++++++++++++++++++++++ src/cowboy_req.erl | 30 ++++++++++ test/handlers/echo_h.erl | 4 ++ test/req_SUITE.erl | 21 +++++++ 5 files changed, 126 insertions(+) create mode 100644 doc/src/manual/cowboy_req.filter_cookies.asciidoc diff --git a/doc/src/manual/cowboy_req.asciidoc b/doc/src/manual/cowboy_req.asciidoc index ca99f9f..f6f4127 100644 --- a/doc/src/manual/cowboy_req.asciidoc +++ b/doc/src/manual/cowboy_req.asciidoc @@ -53,6 +53,7 @@ Processed request: * link:man:cowboy_req:parse_qs(3)[cowboy_req:parse_qs(3)] - Parse the query string * link:man:cowboy_req:match_qs(3)[cowboy_req:match_qs(3)] - Match the query string against constraints * link:man:cowboy_req:parse_header(3)[cowboy_req:parse_header(3)] - Parse the given HTTP header +* link:man:cowboy_req:filter_cookies(3)[cowboy_req:filter_cookies(3)] - Filter cookie headers * link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)] - Parse cookie headers * link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] - Match cookies against constraints * link:man:cowboy_req:binding(3)[cowboy_req:binding(3)] - Access a value bound from the route diff --git a/doc/src/manual/cowboy_req.filter_cookies.asciidoc b/doc/src/manual/cowboy_req.filter_cookies.asciidoc new file mode 100644 index 0000000..20d0a0c --- /dev/null +++ b/doc/src/manual/cowboy_req.filter_cookies.asciidoc @@ -0,0 +1,70 @@ += cowboy_req:filter_cookies(3) + +== Name + +cowboy_req:filter_cookies - Filter cookie headers + +== Description + +[source,erlang] +---- +filter_cookies(Names, Req) -> Req + +Names :: [atom() | binary()] +---- + +Filter cookie headers. + +This function is meant to be used before attempting to parse +or match cookies in order to remove cookies that are not +relevant and are potentially malformed. Because Cowboy by +default crashes on malformed cookies, this function allows +processing requests that would otherwise result in a 400 +error. + +Malformed cookies are unfortunately fairly common due to +the string-based interface provided by browsers and this +function provides a middle ground between Cowboy's strict +behavior and chaotic real world use cases. + +Note that there may still be crashes even after filtering +cookies because this function does not correct malformed +values. Cookies that have malformed values should probably +be unset in an error response or in a redirect. + +This function can be called even if there are no cookies +in the request. + +== Arguments + +Names:: + +The cookies that should be kept. + +Req:: + +The Req object. + +== Return value + +The Req object is returned with its cookie header value +filtered. + +== Changelog + +* *2.7*: Function introduced. + +== Examples + +.Filter then parse cookies +[source,erlang] +---- +Req = cowboy_req:filter_cookies([session_id, token], Req0), +Cookies = cowboy_req:parse_cookies(Req). +---- + +== See also + +link:man:cowboy_req(3)[cowboy_req(3)], +link:man:cowboy_req:parse_cookies(3)[cowboy_req:parse_cookies(3)], +link:man:cowboy_req:match_cookies(3)[cowboy_req:match_cookies(3)] diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index e5ec4a7..8ae090e 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -44,6 +44,7 @@ -export([headers/1]). -export([parse_header/2]). -export([parse_header/3]). +-export([filter_cookies/2]). -export([parse_cookies/1]). -export([match_cookies/2]). @@ -450,6 +451,35 @@ parse_header(Name, Req, Default, ParseFun) -> Value -> ParseFun(Value) end. +-spec filter_cookies([atom() | binary()], Req) -> Req when Req::req(). +filter_cookies(Names0, Req=#{headers := Headers}) -> + Names = [if + is_atom(N) -> atom_to_binary(N, utf8); + true -> N + end || N <- Names0], + case header(<<"cookie">>, Req) of + undefined -> Req; + Value0 -> + Cookies0 = binary:split(Value0, <<$;>>), + Cookies = lists:filter(fun(Cookie) -> + lists:member(cookie_name(Cookie), Names) + end, Cookies0), + Value = iolist_to_binary(lists:join($;, Cookies)), + Req#{headers => Headers#{<<"cookie">> => Value}} + end. + +%% This is a specialized function to extract a cookie name +%% regardless of whether the name is valid or not. We skip +%% whitespace at the beginning and take whatever's left to +%% be the cookie name, up to the = sign. +cookie_name(<<$\s, Rest/binary>>) -> cookie_name(Rest); +cookie_name(<<$\t, Rest/binary>>) -> cookie_name(Rest); +cookie_name(Name) -> cookie_name(Name, <<>>). + +cookie_name(<<>>, Name) -> Name; +cookie_name(<<$=, _/bits>>, Name) -> Name; +cookie_name(<>, Acc) -> cookie_name(Rest, <>). + -spec parse_cookies(req()) -> [{binary(), binary()}]. parse_cookies(Req) -> parse_header(<<"cookie">>, Req). diff --git a/test/handlers/echo_h.erl b/test/handlers/echo_h.erl index 7d0e75b..1b672d1 100644 --- a/test/handlers/echo_h.erl +++ b/test/handlers/echo_h.erl @@ -92,6 +92,10 @@ echo(<<"match">>, Req, Opts) -> Match end, {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; +echo(<<"filter_then_parse_cookies">>, Req0, Opts) -> + Req = cowboy_req:filter_cookies([cake, color], Req0), + Value = cowboy_req:parse_cookies(Req), + {ok, cowboy_req:reply(200, #{}, value_to_iodata(Value), Req), Opts}; echo(What, Req, Opts) -> Key = binary_to_atom(What, latin1), Value = case cowboy_req:path(Req) of diff --git a/test/req_SUITE.erl b/test/req_SUITE.erl index 2cc8de4..76abba9 100644 --- a/test/req_SUITE.erl +++ b/test/req_SUITE.erl @@ -286,6 +286,27 @@ parse_cookies(Config) -> [{<<"cookie">>, "goodname=strawberry\tmilkshake"}], Config), ok. +filter_then_parse_cookies(Config) -> + doc("Filter cookies then parse them."), + <<"[]">> = do_get_body("/filter_then_parse_cookies", Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> + = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry"}], Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> + = do_get_body("/filter_then_parse_cookies", [{<<"cookie">>, "cake=strawberry; color=blue"}], Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>},{<<\"color\">>,<<\"blue\">>}]">> + = do_get_body("/filter_then_parse_cookies", + [{<<"cookie">>, "cake=strawberry"}, {<<"cookie">>, "color=blue"}], Config), + <<"[]">> + = do_get_body("/filter_then_parse_cookies", + [{<<"cookie">>, "bad name=strawberry"}], Config), + <<"[{<<\"cake\">>,<<\"strawberry\">>}]">> + = do_get_body("/filter_then_parse_cookies", + [{<<"cookie">>, "bad name=strawberry; cake=strawberry"}], Config), + <<"[]">> + = do_get_body("/filter_then_parse_cookies", + [{<<"cookie">>, "Blocked by http://www.example.com/upgrade-to-remove"}], Config), + ok. + parse_header(Config) -> doc("Parsed request header with/without default."), <<"[{{<<\"text\">>,<<\"html\">>,[]},1000,[]}]">> -- cgit v1.2.3