authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-04-20 12:07:51+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-04-20 12:07:51+02:00
logac7e895df0f7e21d0b0dbda1d3e27467d4efe8d3
tree2240b94cb938f64c7082f62c5f41798820c161ca
parent4e2147d14e34e6891f2a8f2e2c7f28fdb6d92c2a

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.

2 files changed, 77 insertions(+), 1 deletions(-)

lib/std/crypto.zig+1
...@@ -367,6 +367,7 @@ test {...@@ -367,6 +367,7 @@ test {
367 _ = ff;367 _ = ff;
368 _ = errors;368 _ = errors;
369 _ = tls;369 _ = tls;
370 _ = tls.Client;
370 _ = Certificate;371 _ = Certificate;
371 _ = codecs;372 _ = codecs;
372}373}
lib/std/crypto/tls/Client.zig+76-1
...@@ -382,7 +382,9 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client...@@ -382,7 +382,9 @@ pub fn init(input: *Reader, output: *Writer, options: Options) InitError!Client
382 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, record_header, nonce, pv.server_handshake_key) catch382 P.AEAD.decrypt(cleartext, ciphertext, auth_tag, record_header, nonce, pv.server_handshake_key) catch
383 return error.TlsBadRecordMac;383 return error.TlsBadRecordMac;
384 // TODO use scalar, non-slice version384 // TODO use scalar, non-slice version
385 cleartext_fragment_end += mem.trimEnd(u8, cleartext, "\x00").len;385 const trimmed_len = mem.trimEnd(u8, cleartext, "\x00").len;
386 if (trimmed_len == 0) return error.TlsDecodeError;
387 cleartext_fragment_end += trimmed_len;
386 },388 },
387 }389 }
388 read_seq += 1;390 read_seq += 1;
...@@ -1176,6 +1178,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1176,6 +1178,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1176 .tls_1_3 => {1178 .tls_1_3 => {
1177 const pv = &p.tls_1_3;1179 const pv = &p.tls_1_3;
1178 const P = @TypeOf(p.*);1180 const P = @TypeOf(p.*);
1181 if (record_len < P.AEAD.tag_length) return failRead(c, error.TlsRecordOverflow);
1179 const ad = input.take(tls.record_header_len) catch unreachable; // already peeked1182 const ad = input.take(tls.record_header_len) catch unreachable; // already peeked
1180 const ciphertext_len = record_len - P.AEAD.tag_length;1183 const ciphertext_len = record_len - P.AEAD.tag_length;
1181 const ciphertext = input.take(ciphertext_len) catch unreachable; // already peeked1184 const ciphertext = input.take(ciphertext_len) catch unreachable; // already peeked
...@@ -1192,6 +1195,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {...@@ -1192,6 +1195,7 @@ fn readIndirect(c: *Client) Reader.Error!usize {
1192 return failRead(c, error.TlsBadRecordMac);1195 return failRead(c, error.TlsBadRecordMac);
1193 // TODO use scalar, non-slice version1196 // TODO use scalar, non-slice version
1194 const msg = mem.trimEnd(u8, cleartext, "\x00");1197 const msg = mem.trimEnd(u8, cleartext, "\x00");
1198 if (msg.len == 0) return failRead(c, error.TlsDecodeError);
1195 break :cleartext .{ msg.len - 1, @enumFromInt(msg[msg.len - 1]) };1199 break :cleartext .{ msg.len - 1, @enumFromInt(msg[msg.len - 1]) };
1196 },1200 },
1197 .tls_1_2 => {1201 .tls_1_2 => {
...@@ -1668,3 +1672,74 @@ else...@@ -1668,3 +1672,74 @@ else
1668 .AES_256_GCM_SHA384,1672 .AES_256_GCM_SHA384,
1669 .ECDHE_RSA_WITH_AES_256_GCM_SHA384,1673 .ECDHE_RSA_WITH_AES_256_GCM_SHA384,
1670 });1674 });
1675
1676fn testReadError(input_buf: []const u8, cipher: tls.ApplicationCipher) ReadError {
1677 var input_reader: Reader = .fixed(input_buf);
1678 var read_buf: [tls.max_ciphertext_record_len]u8 = undefined;
1679 var c: Client = .{
1680 .input = &input_reader,
1681 .reader = .{
1682 .buffer = &read_buf,
1683 .vtable = &.{ .stream = stream, .readVec = readVec },
1684 .seek = 0,
1685 .end = 0,
1686 },
1687 .output = undefined,
1688 .writer = undefined,
1689 .tls_version = .tls_1_3,
1690 .read_seq = 0,
1691 .write_seq = 0,
1692 .received_close_notify = false,
1693 .allow_truncation_attacks = false,
1694 .application_cipher = cipher,
1695 .ssl_key_log = null,
1696 };
1697 var w: Writer = .failing;
1698 std.testing.expectError(error.ReadFailed, c.reader.stream(&w, .unlimited)) catch
1699 @panic("expected ReadFailed");
1700 return c.read_err.?;
1701}
1702
1703test "empty inner plaintext" {
1704 const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305;
1705 const key: [AEAD.key_length]u8 = @splat(0);
1706 const iv: [AEAD.nonce_length]u8 = @splat(0);
1707
1708 const plaintext = [1]u8{0x00};
1709 var ciphertext: [plaintext.len]u8 = undefined;
1710 var tag: [AEAD.tag_length]u8 = undefined;
1711 const content_len: u16 = plaintext.len + AEAD.tag_length;
1712 const record_header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(content_len));
1713 AEAD.encrypt(&ciphertext, &tag, &plaintext, &record_header, iv, key);
1714
1715 try std.testing.expectEqual(error.TlsDecodeError, testReadError(
1716 &record_header ++ ciphertext ++ tag,
1717 .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{
1718 .server_key = key,
1719 .server_iv = iv,
1720 .client_secret = undefined,
1721 .server_secret = undefined,
1722 .client_key = undefined,
1723 .client_iv = undefined,
1724 } } },
1725 ));
1726}
1727
1728test "record shorter than tag" {
1729 const AEAD = crypto.aead.chacha_poly.ChaCha20Poly1305;
1730 const record_len: u16 = AEAD.tag_length - 1;
1731 const header = [_]u8{ 0x17, 0x03, 0x03 } ++ mem.toBytes(big(record_len));
1732 const wire = header ++ @as([record_len]u8, @splat(0));
1733
1734 try std.testing.expectEqual(error.TlsRecordOverflow, testReadError(
1735 &wire,
1736 .{ .CHACHA20_POLY1305_SHA256 = .{ .tls_1_3 = .{
1737 .server_key = undefined,
1738 .server_iv = undefined,
1739 .client_secret = undefined,
1740 .server_secret = undefined,
1741 .client_key = undefined,
1742 .client_iv = undefined,
1743 } } },
1744 ));
1745}