Skip to content

Commit 96e980b

Browse files
committed
gzip/inflate: harden GZIP header parsing and ISIZE validation
- gzs(): reject reserved FLG bits (0xE0); add bounds checks for FEXTRA length field before reading it to prevent out-of-bounds access on truncated headers - gzl(): reject ISIZE values above Int range (>= 0x80000000); now raises FzipError instead of returning a negative Int - gunzip_sync(): validate ISIZE against max_output_size before allocating the output buffer; verify that inflate wrote exactly isize bytes and raise InvalidZipData on mismatch; call trim_buf() to return a correctly sized slice when a caller-supplied oversized buffer is used - inflate/inflt(): reject code-length repeat symbol 16 (copy previous) when it appears at index 0 (no previous value); reject repeat counts that would overflow the code-length table - Update GunzipOptions doc comment to reflect that ISIZE is now checked against max_output_size and that max_output_size is a hard limit - Add tests: ISIZE > max_output_size before allocation, ISIZE mismatch after inflate, oversized caller buffer trimmed correctly, FEXTRA truncated length, FEXTRA length exceeds input, reserved FLG bits rejected, code-length repeat 16 at index 0, repeat count overflow
1 parent ad3eea5 commit 96e980b

6 files changed

Lines changed: 144 additions & 16 deletions

File tree

src/gzip.mbt

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,19 @@ fn gzs(d : FixedArray[Byte]) -> Int raise FzipError {
3636
raise fzip_err(InvalidHeader, msg="invalid gzip data")
3737
}
3838
let flg = d[3].to_int()
39+
if (flg & 0xE0) != 0 {
40+
raise fzip_err(InvalidHeader, msg="invalid gzip data")
41+
}
3942
let mut st = 10
4043
if (flg & 4) != 0 {
41-
st += (d[10].to_int() | (d[11].to_int() << 8)) + 2
44+
if st > d.length() - 2 {
45+
raise fzip_err(InvalidHeader, msg="invalid gzip data")
46+
}
47+
let extra_len = d[st].to_int() | (d[st + 1].to_int() << 8)
48+
st += extra_len + 2
49+
if st > d.length() {
50+
raise fzip_err(InvalidHeader, msg="invalid gzip data")
51+
}
4252
}
4353
// skip FNAME and FCOMMENT
4454
let mut zs = ((flg >> 3) & 1) + ((flg >> 4) & 1)
@@ -56,15 +66,13 @@ fn gzs(d : FixedArray[Byte]) -> Int raise FzipError {
5666
}
5767

5868
///|
59-
/// Read GZIP original (uncompressed) size from footer
60-
fn gzl(d : FixedArray[Byte]) -> Int {
61-
let l = d.length()
62-
(d[l - 4].to_int() |
63-
(d[l - 3].to_int() << 8) |
64-
(d[l - 2].to_int() << 16) |
65-
(d[l - 1].to_int() << 24))
66-
.reinterpret_as_uint()
67-
.reinterpret_as_int()
69+
/// Read a sync-API-safe GZIP ISIZE footer value.
70+
fn gzl(d : FixedArray[Byte]) -> Int raise FzipError {
71+
let raw = b4(d, d.length() - 4)
72+
if raw > max_int_val().reinterpret_as_uint() {
73+
raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds Int range")
74+
}
75+
raw.reinterpret_as_int()
6876
}
6977

7078
///|
@@ -116,16 +124,20 @@ pub fn gunzip_sync(
116124
if st + 8 > data.length() {
117125
raise fzip_err(InvalidHeader, msg="invalid gzip data")
118126
}
127+
let isize = gzl(data)
128+
if isize > opts.max_output_size {
129+
raise fzip_err(InvalidZipData, msg="gzip ISIZE exceeds max_output_size")
130+
}
119131
let out = match opts.out {
120132
Some(o) => o
121-
None => FixedArray::make(gzl(data), b'\x00')
133+
None => FixedArray::make(isize, b'\x00')
122134
}
123135
let crc_st : CRC32State? = if opts.verify_checksum {
124136
Some(CRC32State::new())
125137
} else {
126138
None
127139
}
128-
let (buf, _len) = inflt(
140+
let (buf, len) = inflt(
129141
data,
130142
InflateState::new(2),
131143
Some(out),
@@ -145,5 +157,8 @@ pub fn gunzip_sync(
145157
}
146158
None => ()
147159
}
148-
buf
160+
if len != isize {
161+
raise fzip_err(InvalidZipData, msg="gzip ISIZE mismatch")
162+
}
163+
trim_buf(buf, len)
149164
}

src/gzip_wbtest.mbt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,61 @@ test "gunzip with verify_checksum=true detects corruption" {
9090
assert_true(result == Some(InvalidChecksum), msg="expected InvalidChecksum")
9191
}
9292

93+
///|
94+
test "gunzip - rejects ISIZE above max_output_size before allocation" {
95+
let data : FixedArray[Byte] = [b'H', b'i']
96+
let compressed = gzip_sync(data)
97+
w4(compressed, compressed.length() - 4, 1000U)
98+
let result : Result[FixedArray[Byte], FzipError] = try? gunzip_sync(
99+
compressed,
100+
opts={
101+
out: None,
102+
dictionary: None,
103+
max_output_size: 16,
104+
max_input_size: default_max_input_size,
105+
verify_checksum: true,
106+
},
107+
)
108+
guard result is Err(FzipError(code~, message~)) else {
109+
fail("expected error")
110+
}
111+
assert_eq(code, InvalidZipData)
112+
assert_true(message.contains("ISIZE exceeds max_output_size"))
113+
}
114+
115+
///|
116+
test "gunzip - rejects ISIZE mismatch after successful inflate" {
117+
let data : FixedArray[Byte] = [b'H', b'e', b'l', b'l', b'o']
118+
let compressed = gzip_sync(data)
119+
w4(compressed, compressed.length() - 4, 8U)
120+
let result : Result[FixedArray[Byte], FzipError] = try? gunzip_sync(
121+
compressed,
122+
)
123+
guard result is Err(FzipError(code~, message~)) else {
124+
fail("expected error")
125+
}
126+
assert_eq(code, InvalidZipData)
127+
assert_true(message.contains("ISIZE mismatch"))
128+
}
129+
130+
///|
131+
test "gunzip - trims oversized caller output buffer" {
132+
let data : FixedArray[Byte] = [b'H', b'e', b'l', b'l', b'o']
133+
let compressed = gzip_sync(data)
134+
let out = FixedArray::make(32, b'\x00')
135+
let result = gunzip_sync(compressed, opts={
136+
out: Some(out),
137+
dictionary: None,
138+
max_output_size: default_max_output_size,
139+
max_input_size: default_max_input_size,
140+
verify_checksum: true,
141+
})
142+
assert_eq(result.length(), data.length())
143+
for i in 0..<data.length() {
144+
assert_eq(result[i], data[i])
145+
}
146+
}
147+
93148
///|
94149
test "gzip level 9" {
95150
let data : FixedArray[Byte] = [1, 2, 3, 4, 5]

src/inflate.mbt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,9 @@ fn inflt(
229229
let mut c = 0
230230
let mut n = 0
231231
if s == 16 {
232+
if i == 0 {
233+
raise fzip_err(InvalidLengthLiteral)
234+
}
232235
while bcnt < 2 && bpos < dat_end {
233236
bbuf = bbuf | (dat[bpos].to_int() << bcnt)
234237
bpos += 1
@@ -257,6 +260,9 @@ fn inflt(
257260
bbuf = bbuf >> 7
258261
bcnt -= 7
259262
}
263+
if n > tl - i {
264+
raise fzip_err(InvalidLengthLiteral)
265+
}
260266
while n > 0 {
261267
ldt[i] = c.to_byte()
262268
i += 1

src/inflate_wbtest.mbt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,28 @@ test "inflate - truncated data error" {
371371
assert_true(result is Err(_))
372372
}
373373

374+
///|
375+
test "inflate - rejects code length repeat 16 at start" {
376+
let malformed : FixedArray[Byte] = [b'\x05', b'\x00', b'\x02', b'\x00']
377+
let result : Result[FixedArray[Byte], FzipError] = try? inflate_sync(
378+
malformed,
379+
)
380+
guard result is Err(FzipError(code~, ..)) else { fail("expected error") }
381+
assert_eq(code, InvalidLengthLiteral)
382+
}
383+
384+
///|
385+
test "inflate - rejects code length repeat overflow" {
386+
let malformed : FixedArray[Byte] = [
387+
b'\x05', b'\x00', b'\x80', b'\xC0', b'\xDF', b'\x1F',
388+
]
389+
let result : Result[FixedArray[Byte], FzipError] = try? inflate_sync(
390+
malformed,
391+
)
392+
guard result is Err(FzipError(code~, ..)) else { fail("expected error") }
393+
assert_eq(code, InvalidLengthLiteral)
394+
}
395+
374396
///|
375397
test "inflate - custom max_output_size limit" {
376398
// Test that max_output_size is enforced

src/security_wbtest.mbt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,36 @@ test "security/gzip - FNAME flag but truncated" {
3232
assert_true(result is Err(_))
3333
}
3434

35+
///|
36+
test "security/gzip - FEXTRA flag but truncated length" {
37+
let truncated : FixedArray[Byte] = [
38+
b'\x1F', b'\x8B', b'\x08', b'\x04', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
39+
b'\x03',
40+
]
41+
let result = try? gunzip_sync(truncated)
42+
assert_true(result is Err(_))
43+
}
44+
45+
///|
46+
test "security/gzip - FEXTRA length exceeds input" {
47+
let truncated : FixedArray[Byte] = [
48+
b'\x1F', b'\x8B', b'\x08', b'\x04', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
49+
b'\x03', b'\x10', b'\x00',
50+
]
51+
let result = try? gunzip_sync(truncated)
52+
assert_true(result is Err(_))
53+
}
54+
55+
///|
56+
test "security/gzip - reserved flags rejected" {
57+
let invalid : FixedArray[Byte] = [
58+
b'\x1F', b'\x8B', b'\x08', b'\xE0', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
59+
b'\x03', b'\x03', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
60+
]
61+
let result = try? gunzip_sync(invalid)
62+
assert_true(result is Err(_))
63+
}
64+
3565
///|
3666
test "security/zlib - empty input" {
3767
let empty : FixedArray[Byte] = []

src/types.mbt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ pub fn GzipOptions::default() -> GzipOptions {
8484
/// By default fzip verifies the CRC-32 footer to detect corrupted data. Disable
8585
/// `verify_checksum` only when data integrity is already guaranteed elsewhere
8686
/// and decompression speed is more important. When `out` is not supplied,
87-
/// `gunzip_sync` allocates its output from the GZIP ISIZE footer; callers that
88-
/// need a strict output cap should provide an appropriately sized `out` buffer.
87+
/// `gunzip_sync` allocates its output from the GZIP ISIZE footer after checking
88+
/// it against `max_output_size`.
8989
pub(all) struct GunzipOptions {
9090
/// Optional pre-allocated output buffer; when supplied, it must fit the full output.
9191
out : FixedArray[Byte]?
9292
/// Optional preset dictionary used by the inner DEFLATE stream.
9393
dictionary : FixedArray[Byte]?
94-
/// Output size limit used only when the inner inflater grows its buffer.
94+
/// Maximum allowed uncompressed output size.
9595
max_output_size : Int
9696
/// Maximum allowed compressed input size.
9797
max_input_size : Int

0 commit comments

Comments
 (0)