@@ -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 ) >>
0 commit comments