From ac7e895df0f7e21d0b0dbda1d3e27467d4efe8d3 Mon Sep 17 00:00:00 2001 From: Frank Denis Date: Mon, 20 Apr 2026 12:07:51 +0200 Subject: [PATCH] tls.Client: reject empty TLS 1.3 inner plaintext and short records After decryption, TLS 1.3 plaintext is trimmed of zero padding, then the last byte is read as the content type. But when the plaintext was entirely zero padding, we got a "thread panic: integer overflow at msg.len - 1" error. That could be triggered by any server to crash the client. --- lib/std/crypto.zig | 1 + lib/std/crypto/tls/Client.zig | 77 ++++++++++++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/lib/std/crypto.zig b/lib/std/crypto.zig index d020d61e14c9c3d27712ed05bacf03d16ca1c5b6..c12a085ecedf3e50c4523c9b06f90e35b68e02ed 100644 --- a/lib/std/crypto.zig +++ b/lib/std/crypto.zig @@ -367,6 +367,7 @@ test { _ = ff; _ = errors; _ = tls; + _ = tls.Client; _ = Certificate; _ = codecs; } diff --git a/lib/std/crypto/tls/Client.zig b/lib/std/crypto/tls/Client.zig index 93251e5137dd6a74c582e90b7a82409154363d0e..4c547964efa57416e6f834244488e6d88b08a0f2 100644 --- a/lib/std/crypto/tls/Client.zig +++ b/lib/std/crypto/tls/Client.zig @@ -382,7 +382,9 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client P.AEAD.decrypt(cleartext, ciphertext, auth_tag, record_header, nonce, pv.server_handshake_key) catch return error.TlsBadRecordMac; // TODO use scalar, non-slice version - cleartext_fragment_end += mem.trimEnd(u8, cleartext, "\x00").len; + const trimmed_len = mem.trimEnd(u8, cleartext, "\x00").len; + if (trimmed_len == 0) return error.TlsDecodeError; + cleartext_fragment_end += trimmed_len; }, } read_seq += 1; @@ -1176,6 +1178,7 @@ fn readIndirect(c: *Client) Reader.Error!usize { .tls_1_3 => { const pv = &p.tls_1_3; const P = @TypeOf(p.*); + if (record_len < P.AEAD.tag_length) return failRead(c, error.TlsRecordOverflow); const ad = input.take(tls.record_header_len) catch unreachable; // already peeked const ciphertext_len = record_len - P.AEAD.tag_length; const ciphertext = input.take(ciphertext_len) catch unreachable; // already peeked @@ -1192,6 +1195,7 @@ fn readIndirect(c: *Client) Reader.Error!usize { return failRead(c, error.TlsBadRecordMac); // TODO use scalar, non-slice version const msg = mem.trimEnd(u8, cleartext, "\x00"); + if (msg.len == 0) return failRead(c, error.TlsDecodeError); break :cleartext .{ msg.len - 1, @enumFromInt(msg[msg.len - 1]) }; }, .tls_1_2 => { @@ -1668,3 +1672,74 @@ else .AES_256_GCM_SHA384, .ECDHE_RSA_WITH_AES_256_GCM_SHA384, }); + +fn testReadError(input_buf: []const u8, cipher: tls.ApplicationCipher) ReadError { + var input_reader: Reader = .fixed(input_buf); + var read_buf: [tls.max_ciphertext_record_len]u8 = undefined; + var c: Client = .{ + .input = &input_reader, + .reader = .{ + .buffer = &read_buf, + .vtable = &.{ .stream = stream, .readVec = readVec }, + .seek = 0, + .end = 0, + }, + .output = undefined, + .writer = undefined, + .tls_version = .tls_1_3, + .read_seq = 0, + .write_seq = 0, + .received_close_notify = false, + .allow_truncation_attacks = false, + .application_cipher = cipher, + .ssl_key_log = null, + }; + var w: Writer = .failing; + std.testing.expectError(error.ReadFailed, c.reader.stream(&w, .unlimited)) catch + @panic("expected ReadFailed"); + return c.read_err.?; +} + +test "empty inner plaintext" { + const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305; + const key: [AEAD.key_length]u8 = @splat(0); + const iv: [AEAD.nonce_length]u8 = @splat(0); + + const plaintext = [1]u8{0x00}; + var ciphertext: [plaintext.len]u8 = undefined; + var tag: [AEAD.tag_length]u8 = undefined; + const content_len: u16 = plaintext.len + AEAD.tag_length; + const record_header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(content_len)); + AEAD.encrypt(&ciphertext, &tag, &plaintext, &record_header, iv, key); + + try std.testing.expectEqual(error.TlsDecodeError, testReadError( + &record_header ++ ciphertext ++ tag, + .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{ + .server_key = key, + .server_iv = iv, + .client_secret = undefined, + .server_secret = undefined, + .client_key = undefined, + .client_iv = undefined, + } } }, + )); +} + +test "record shorter than tag" { + const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305; + const record_len: u16 = AEAD.tag_length - 1; + const header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(record_len)); + const wire = header ++ @as([record_len]u8, @splat(0)); + + try std.testing.expectEqual(error.TlsRecordOverflow, testReadError( + &wire, + .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{ + .server_key = undefined, + .server_iv = undefined, + .client_secret = undefined, + .server_secret = undefined, + .client_key = undefined, + .client_iv = undefined, + } } }, + )); +} -- 2.54.0