# 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`.