Commit 10beb19ce7

Andrew Kelley <andrew@ziglang.org>
2024-02-23 02:52:00
std.http: assert against \r\n in headers
The HTTP specification does not provide a way to escape \r\n in headers, so it's the API user's responsibility to ensure the header names and values do not contain \r\n. Also header names must not contain ':'. It's an assertion, not an error, because the calling code very likely is using hard-coded values or server-provided values that do not need to be checked, and the error would be unreachable anyway. Untrusted user input must not be put directly into into HTTP headers.
1 parent d051b13
Changed files (2)
lib/std/http/Client.zig
@@ -1505,12 +1505,26 @@ pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{
 ///
 /// The caller is responsible for calling `deinit()` on the `Request`.
 /// This function is threadsafe.
+///
+/// Asserts that "\r\n" does not occur in any header name or value.
 pub fn open(
     client: *Client,
     method: http.Method,
     uri: Uri,
     options: RequestOptions,
 ) RequestError!Request {
+    if (std.debug.runtime_safety) {
+        for (options.extra_headers) |header| {
+            assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
+            assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
+            assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
+        }
+        for (options.privileged_headers) |header| {
+            assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
+            assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
+        }
+    }
+
     const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme;
 
     const port: u16 = uri.port orelse switch (protocol) {
lib/std/http/Server.zig
@@ -296,6 +296,7 @@ pub const Request = struct {
     ///
     /// Asserts status is not `continue`.
     /// Asserts there are at most 25 extra_headers.
+    /// Asserts that "\r\n" does not occur in any header name or value.
     pub fn respond(
         request: *Request,
         content: []const u8,
@@ -304,6 +305,13 @@ pub const Request = struct {
         const max_extra_headers = 25;
         assert(options.status != .@"continue");
         assert(options.extra_headers.len <= max_extra_headers);
+        if (std.debug.runtime_safety) {
+            for (options.extra_headers) |header| {
+                assert(std.mem.indexOfScalar(u8, header.name, ':') == null);
+                assert(std.mem.indexOfPosLinear(u8, header.name, 0, "\r\n") == null);
+                assert(std.mem.indexOfPosLinear(u8, header.value, 0, "\r\n") == null);
+            }
+        }
 
         const transfer_encoding_none = (options.transfer_encoding orelse .chunked) == .none;
         const server_keep_alive = !transfer_encoding_none and options.keep_alive;
@@ -765,7 +773,7 @@ pub const Response = struct {
     /// Respects the value of `elide_body` to omit all data after the headers.
     /// Asserts there are at most 25 trailers.
     pub fn endChunked(r: *Response, options: EndChunkedOptions) WriteError!void {
-        assert(r.content_length == null);
+        assert(r.transfer_encoding == .chunked);
         try flush_chunked(r, options.trailers);
         r.* = undefined;
     }