Skip to content

Commit 274eb55

Browse files
authored
Lazily precompile the invalid-field-value match pattern (#662)
validate_field_value! rebuilt its ["\r", "\n", "\0"] match pattern on every header via String.contains?/2, which recompiles an Aho-Corasick automaton on each call regardless of value length. Cache the compiled pattern in :persistent_term instead, initializing it lazily on first use so it doesn't need any application-startup wiring. Fixes #629
1 parent 10710de commit 274eb55

1 file changed

Lines changed: 18 additions & 2 deletions

File tree

lib/bandit/http1/socket.ex

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ defmodule Bandit.HTTP1.Socket do
5050

5151
@max_chunk_size_byte_count 16
5252

53+
@invalid_field_value_pattern_key {__MODULE__, :invalid_field_value_pattern}
54+
5355
def peer_data(%@for{} = socket), do: Bandit.SocketHelpers.peer_data(socket.socket)
5456

5557
def sock_data(%@for{} = socket), do: Bandit.SocketHelpers.sock_data(socket.socket)
@@ -187,16 +189,30 @@ defmodule Bandit.HTTP1.Socket do
187189
end
188190

189191
# RFC9110§5.5: field values containing CR, LF, or NUL characters are invalid and
190-
# dangerous (a common request-smuggling / response-splitting vector).
192+
# dangerous (a common request-smuggling / response-splitting vector). The match
193+
# pattern is compiled once (lazily, on first use) and cached in :persistent_term,
194+
# since compiling it on every call is a measurable fraction of header parsing time.
191195
@spec validate_field_value!(binary()) :: :ok
192196
defp validate_field_value!(value) do
193-
if String.contains?(value, ["\r", "\n", "\0"]) do
197+
if :binary.match(value, invalid_field_value_pattern()) != :nomatch do
194198
request_error!("Field value contains invalid characters (RFC9110§5.5)")
195199
else
196200
:ok
197201
end
198202
end
199203

204+
defp invalid_field_value_pattern do
205+
case :persistent_term.get(@invalid_field_value_pattern_key, :undefined) do
206+
:undefined ->
207+
pattern = :binary.compile_pattern(["\r", "\n", "\0"])
208+
:persistent_term.put(@invalid_field_value_pattern_key, pattern)
209+
pattern
210+
211+
pattern ->
212+
pattern
213+
end
214+
end
215+
200216
defp get_content_length!(headers) do
201217
case Bandit.Headers.get_content_length(headers) do
202218
{:ok, content_length} -> content_length

0 commit comments

Comments
 (0)