# Spoofy Writeup Spoofy was solvable with duplicate headers and I'm sure you can find many writeups with a solution, but I want to explain why it works, since the vuln was easily guessable without appreciation for the origin. Heroku uses their own erlang HTTP Proxy vegur (https://github.com/heroku/vegur). In `vegur_proxy_middleware.erl`, they append the connecting ip to `X-Forwarded-For` using `vegur_utils:add_or_append_header` (https://github.com/heroku/vegur/blob/26cf07b6d7f12841e529cd2a9fc354a70927a6be/src/vegur_proxy_middleware.erl#L236) ```erlang {Headers2, Req3} = vegur_utils:add_or_append_header(<<"x-forwarded-for">>, inet:ntoa(PeerAddress), Headers1, Req2), ``` `add_or_append_header` is defined as such: (https://github.com/heroku/vegur/blob/26cf07b6d7f12841e529cd2a9fc354a70927a6be/src/vegur_utils.erl#L116-L129) ```erlang -spec add_or_append_header(Key, Value, Headers, Req) -> {Headers, Req} when Key :: iodata(), Value :: iodata(), Headers :: [{iodata(), iodata()}]|[], Req :: cowboyku_req:req(). add_or_append_header(Key, Val, Headers, Req) -> case cowboyku_req:header(Key, Req) of {undefined, Req2} -> {Headers ++ [{Key, Val}], Req2}; {CurrentVal, Req2} -> {lists:keyreplace(Key, 1, Headers, {Key, [CurrentVal, ", ", Val]}), Req2} end. ``` Erlang (and elixir) likes to use key-value pair lists instead of maps for whatever reason, and most of the stdlib functions just operate on the first matching key. This is an issue, since key-value pair lists don't enforce that keys can't be duplicates. `cowboyku_req:header` is defined as such: (https://github.com/heroku/cowboyku/blob/master/src/cowboyku_req.erl#L372-L378) ```erlang -spec header(binary(), Req, Default) -> {binary() | Default, Req} when Req::req(), Default::any(). header(Name, Req, Default) -> case lists:keyfind(Name, 1, Req#http_req.headers) of {Name, Value} -> {Value, Req}; false -> {Default, Req} end. ``` It finds the first header with the name specified. The `add_or_append_header` function only finds the first `X-Forwarded-For` and only appends the ip to the first `X-Forwarded-For`. Any `X-Forwarded-For`s afterwards are left untouched. Thus, when you send ``` X-Forwarded-For: a X-Forwarded-For: b ``` Heroku transforms it into ``` X-Forwarded-For: a, connectingip X-Forwarded-For: b ``` As per RFC 2616, > Multiple message-header fields with the same field-name MAY be > present in a message if and only if the entire field-value for that > header field is defined as a comma-separated list [i.e., #(values)]. > It MUST be possible to combine the multiple header fields into one > "field-name: field-value" pair, without changing the semantics of the > message, by appending each subsequent field-value to the first, each > separated by a comma. Thus, Flask and many other HTTP servers will concatenate duplicate header names with a comma, leaving the header parsed as: ``` a, connectingip,b ``` So, if ``` X-Forwarded-For: 1.3.3.7 X-Forwarded-For: x, 1.3.3.7 ``` is sent, Heroku transforms it into ``` X-Forwarded-For: 1.3.3.7, connectingip X-Forwarded-For: x, 1.3.3.7 ``` which will be parsed as ``` 1.3.3.7, connectingip,x, 1.3.3.7 ``` which has the same first and last ip with a value of `1.3.3.7`.