Skip to content

Commit c657580

Browse files
committed
Raise if ; is present in cookie attributes
1 parent 981597d commit c657580

4 files changed

Lines changed: 71 additions & 5 deletions

File tree

lib/plug/conn.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1700,7 +1700,9 @@ defmodule Plug.Conn do
17001700
* `:secure` - if the cookie must be sent only over https. Defaults
17011701
to true when the connection is HTTPS
17021702
* `:extra` - string to append to cookie. Use this to take advantage of
1703-
non-standard cookie attributes.
1703+
non-standard cookie attributes. Since this option may append multiple
1704+
attributes, callers must not pass user input. If user input must be
1705+
passed, callers must validate it against semicolon (`;`).
17041706
* `:sign` - when true, signs the cookie
17051707
* `:encrypt` - when true, encrypts the cookie
17061708
* `:same_site` - set the cookie SameSite attribute to a string value.

lib/plug/conn/cookies.ex

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ defmodule Plug.Conn.Cookies do
33
Conveniences for encoding and decoding cookies.
44
"""
55

6+
@invalid_cookie_field [";"]
7+
68
@doc false
79
def sign_or_encrypt(%Plug.Conn{} = conn, key, value, opts) do
810
{sign?, opts} = Keyword.pop(opts, :sign, false)
@@ -92,9 +94,14 @@ defmodule Plug.Conn.Cookies do
9294
defp skip_until_cc(<<_, t::binary>>, acc), do: skip_until_cc(t, acc)
9395
defp skip_until_cc(<<>>, acc), do: acc
9496

95-
@doc """
97+
@doc ~S"""
9698
Encodes the given cookies as expected in a response header.
9799
100+
Raises if the cookie key, value, path, domain, or same-site option contains
101+
semicolon (`;`). It does not validate against control feed (`\r`), newline
102+
(`\n`), or null (`\x00`) characters as this is expected to be done by the
103+
caller when the cookie is added as a header.
104+
98105
## Examples
99106
100107
iex> encode("key1", %{value: "value1"})
@@ -104,21 +111,38 @@ defmodule Plug.Conn.Cookies do
104111
"key1=value1; path=/example; secure"
105112
"""
106113
def encode(key, opts \\ %{}) when is_map(opts) do
114+
invalid_cookie_field = :binary.compile_pattern(@invalid_cookie_field)
115+
107116
value = Map.get(opts, :value)
108117
path = Map.get(opts, :path, "/")
109118

110119
key = to_string(key)
111120
value = to_string(value)
112121
path = to_string(path)
113122

123+
validate_cookie_field!("key", key, invalid_cookie_field)
124+
validate_cookie_field!("value", value, invalid_cookie_field)
125+
validate_cookie_field!("path", path, invalid_cookie_field)
126+
114127
acc = [key, ?=, value, "; path=", path]
115-
acc = if domain = opts[:domain], do: [acc, "; domain=", domain], else: acc
128+
129+
acc =
130+
if domain = opts[:domain],
131+
do: [
132+
acc,
133+
"; domain=",
134+
validate_cookie_field!("domain", to_string(domain), invalid_cookie_field)
135+
],
136+
else: acc
137+
116138
acc = if max_age = opts[:max_age], do: [acc | encode_max_age(max_age, opts)], else: acc
117139
acc = if Map.get(opts, :secure, false), do: [acc | "; secure"], else: acc
118140
acc = if Map.get(opts, :http_only, true), do: [acc | "; HttpOnly"], else: acc
119141

120142
acc =
121-
if same_site = Map.get(opts, :same_site), do: [acc | encode_same_site(same_site)], else: acc
143+
if same_site = Map.get(opts, :same_site),
144+
do: [acc | encode_same_site(same_site, invalid_cookie_field)],
145+
else: acc
122146

123147
acc = if extra = opts[:extra], do: [acc, "; ", extra], else: acc
124148

@@ -131,7 +155,19 @@ defmodule Plug.Conn.Cookies do
131155
["; expires=", rfc2822(time), "; max-age=", Integer.to_string(max_age)]
132156
end
133157

134-
defp encode_same_site(value) when is_binary(value), do: ["; SameSite=", value]
158+
defp encode_same_site(value, invalid_cookie_field) when is_binary(value),
159+
do: ["; SameSite=", validate_cookie_field!("same_site", value, invalid_cookie_field)]
160+
161+
defp validate_cookie_field!(field, value, invalid_cookie_field) do
162+
case :binary.match(value, invalid_cookie_field) do
163+
:nomatch ->
164+
value
165+
166+
_ ->
167+
raise ArgumentError,
168+
"cookie #{field} contains semicolon (;): " <> inspect(value)
169+
end
170+
end
135171

136172
defp pad(n) when n < 10, do: <<?0, ?0 + n>>
137173
defp pad(n), do: <<?0 + div(n, 10), ?0 + rem(n, 10)>>

test/plug/conn/cookies_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,20 @@ defmodule Plug.Conn.CookiesTest do
178178
assert encode("foo", %{value: "bar", extra: "SameSite=Lax"}) ==
179179
"foo=bar; path=/; HttpOnly; SameSite=Lax"
180180
end
181+
182+
test "raises on invalid cookie fields" do
183+
fields = [
184+
{"key", fn value -> encode(value, %{value: "bar"}) end},
185+
{"value", fn value -> encode("foo", %{value: value}) end},
186+
{"path", fn value -> encode("foo", %{value: "bar", path: value}) end},
187+
{"domain", fn value -> encode("foo", %{value: "bar", domain: value}) end},
188+
{"same_site", fn value -> encode("foo", %{value: "bar", same_site: value}) end}
189+
]
190+
191+
for {field, fun} <- fields do
192+
assert_raise ArgumentError, ~r/cookie #{field} contains/, fn ->
193+
fun.("foo;bar")
194+
end
195+
end
196+
end
181197
end

test/plug/conn_test.exs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,18 @@ defmodule Plug.ConnTest do
10991099
|> put_resp_cookie("foo", "bar\nbaz")
11001100
|> send_resp(200, "OK")
11011101
end
1102+
1103+
assert_raise Plug.Conn.InvalidHeaderError, fn ->
1104+
conn(:get, "/")
1105+
|> put_resp_cookie("foo", "bar", domain: "example.com\n")
1106+
|> send_resp(200, "OK")
1107+
end
1108+
1109+
assert_raise Plug.Conn.InvalidHeaderError, fn ->
1110+
conn(:get, "/")
1111+
|> put_resp_cookie("foo", "bar", extra: "SameSite=Lax\n")
1112+
|> send_resp(200, "OK")
1113+
end
11021114
end
11031115

11041116
test "put_resp_cookie/4 is secure on https" do

0 commit comments

Comments
 (0)