Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions lib/req/fields.ex
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,29 @@ defmodule Req.Fields do
end
end

@doc """
Drops the given `names`.
"""
if @legacy? do
def drop(fields, names) when is_binary(name) do
names_to_drop = Enum.map(names, &ensure_name_downcase/1)

for {name, value} <- fields,
name not in names_to_drop do
{name, value}
end
end
else
def drop(fields, names) do
names_to_drop = Enum.map(names, &ensure_name_downcase/1)

for {name, values} <- fields,
name not in names_to_drop do
{name, values}
end
end
end

@doc """
Returns fields as list.
"""
Expand Down
24 changes: 22 additions & 2 deletions lib/req/steps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,27 @@ defmodule Req.Steps do
defp hash_init(:sha1), do: hash_init(:sha)
defp hash_init(type), do: :crypto.hash_init(type)

@aws_sigv4_excluded_headers [
# Services like R2 can rewrite this header when
# encodings it doesn't support are included, i.e. zstd
"accept-encoding",
# Trace ID can be rewritten by AWS infrastructure
"x-amzn-trace-id",
# Authorization is set by SigV4 itself / not part of canonical request
"authorization",
# RFC 2616 Section 13.5.1 "hop-by-hop" headers
# (list is historical; RFC 7230/9110 use Connection header as the
# authoritative mechanism, but this enumeration remains the practical baseline)
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade"
]

@doc """
Signs request with AWS Signature Version 4.

Expand Down Expand Up @@ -1456,8 +1477,7 @@ defmodule Req.Steps do
end

request = Req.Request.put_new_header(request, "host", request.url.host)

headers = for {name, values} <- request.headers, value <- values, do: {name, value}
headers = Req.Fields.drop(request.headers, @aws_sigv4_excluded_headers)

headers =
Req.Utils.aws_sigv4_headers(
Expand Down
64 changes: 64 additions & 0 deletions test/req/steps_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,70 @@ defmodule Req.StepsTest do
assert Req.put!(req).body == "ok"
end

test "excludes accept-encoding, hop-by-hop, and trace-id headers from signature" do
plug = fn conn ->
[authorization] = Plug.Conn.get_req_header(conn, "authorization")

signed_headers =
authorization
|> String.split(",")
|> Enum.find_value(fn part ->
case String.split(part, "=", parts: 2) do
["SignedHeaders", value] -> String.split(value, ";")
_ -> nil
end
end)

for excluded <- [
"accept-encoding",
"x-amzn-trace-id",
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade"
] do
refute excluded in signed_headers,
"expected #{excluded} not in SignedHeaders, got: #{inspect(signed_headers)}"
end

# Headers excluded from the signature are still sent on the wire.
assert ["zstd, br, gzip"] = Plug.Conn.get_req_header(conn, "accept-encoding")
assert ["trace-123"] = Plug.Conn.get_req_header(conn, "x-amzn-trace-id")
assert ["keep-alive"] = Plug.Conn.get_req_header(conn, "connection")

# Non-excluded custom headers are still signed.
assert "x-custom" in signed_headers

Plug.Conn.send_resp(conn, 200, "ok")
end

req =
Req.new(
url: "https://s3.amazonaws.com",
aws_sigv4: [access_key_id: "foo", secret_access_key: "bar"],
headers: [
"x-amzn-trace-id": "trace-123",
connection: "keep-alive",
"keep-alive": "timeout=5",
"proxy-authenticate": "Basic",
"proxy-authorization": "Basic foo",
te: "trailers",
trailer: "Expires",
"transfer-encoding": "chunked",
upgrade: "websocket",
"x-custom": "signed"
],
body: "hello",
plug: plug
)

assert Req.put!(req).body == "ok"
end

test "missing :access_key_id" do
req = Req.new(aws_sigv4: [])

Expand Down
Loading