Repository navigation
[GHSA-jj3q-cwqj-842r] ImageSharp: CCITT fax decompression (T4/Modified Huffman): unbounded WriteBits overflows strip buffer — heap OOB write in SixLabors.ImageSharp - #10272
JimBobSquarePants wants to merge 1 commit into
Conversation
|
Hi there @JimBobSquarePants! A community member has suggested an improvement to your security advisory. If approved, this change will affect the global advisory listed at github.com/advisories. It will not affect the version listed in your project repository. This change will be reviewed by our Security Curation Team. If you have thoughts or feedback, please share them in a comment here! If this PR has already been closed, you can start a new community contribution for this advisory |
There was a problem hiding this comment.
🟡 Changes recommended
The modified timestamp is stale, and the description still identifies v4.1.0 as the latest release.
2 open findings
What changed in this PR
Updates the ImageSharp advisory for the v3 backport release.
Changes:
- Adds v3.2.0 and v4.1.1 patched-version guidance.
- Corrects the NuGet package name.
- Splits v3 and v4 affected ranges.
| File | Description |
|---|---|
GHSA-jj3q-cwqj-842r.json |
Updates package and vulnerability metadata. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| "schema_version": "1.4.0", | ||
| "id": "GHSA-jj3q-cwqj-842r", | ||
| "modified": "2026-10-07T16:18:51Z", | ||
| "modified": "2026-10-07T16:18:52Z", |
| ], | ||
| "summary": "ImageSharp: CCITT fax decompression (T4/Modified Huffman): unbounded WriteBits overflows strip buffer — heap OOB write in SixLabors.ImageSharp", | ||
| "details": "## Summary\n\nWhen decoding a fax-compressed strip TIFF (Compression=3 / Group 3 1D, or Compression=2 / Modified Huffman), the CCITT decompressors write decoded runs through `BitWriterUtils.WriteBits/WriteBit/WriteZeroBit`, which advance and write bits via `Unsafe.Add` with read-modify-write semantics — **without ever comparing the write position against the target buffer length**. The strip buffer is sized `ImageWidth × RowsPerStrip` (8 bytes in the PoC), but two independent defects let an attacker write tens of millions of bits past it: (a) T4 only increments `rowsWritten` when an EOL code is read, and one `ReadNextRun` accumulates unlimited makeup codes (+2560 px per 12-bit code) into a single `RunLength` that `WritePixelRun` then writes in one shot; (b) Modified Huffman writes **before** validating (`pixelsWritten > Width` is checked at :90-93, after the write at :56-63), so the overflow completes even though an exception is thrown later. One crafted ~90 KB file writes ~19.2 MB linearly past an 8-byte buffer and deterministically kills the process; the write offset and length are fully attacker-controlled (classic heap-corruption primitive on the managed heap).\n\nVerified at commit `5cd4d0d26a82a9549f297a237aea9cf665bddff8` (main; latest release v4.1.0, the supported major). A related tiled-path variant (tile-buffer width mismatch) was reported separately as GHSA-v76p-62qx-wwq2 — this report covers the distinct strip-path root cause.\n\n## Details\n\nRoot cause: the bit-writing sink has no bounds check, and neither decompressor constrains run lengths against the strip buffer.\n\n- Unchecked write primitive: [BitWriterUtils.cs#L51](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs#L51) (`WriteBit`), :58 (`WriteZeroBit` — also read-modify-write, so even all-white runs really write), :11 (`WriteBits`)\n- T4 trigger: [T4TiffCompression.cs#L75](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs#L75) and L109-L119 — `rowsWritten` only increments on EOL; consecutive makeup codes accumulate into one `RunLength`, then `WritePixelRun` writes it in full\n- MH trigger: [ModifiedHuffmanTiffCompression.cs#L56-L63](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs#L56-L63) — write happens before the width check at L90-L93\n- Buffer size: [TiffDecoderCore.cs#L932-L967](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs#L932-L967) — `CalculateStripBufferSize` = width × bpp/8 × rowsPerStrip\n\nAttack surface: `Image.Load(stream)` on an attacker-supplied strip TIFF (`TiffDecoderCore.DecodeStripsChunky` → `TiffDecompressorsFactory.Create` → `Decompress`). Default configuration, no authentication, no user interaction — a plain strip TIFF (far more common than the tiled variant) with Compression=2 or 3 and a chain of CCITT makeup codes.\n\nRelationship to GHSA-v76p-62qx-wwq2: that report is the **tiled** variant — a caller-side width mismatch (`TiffDecompressorsFactory` drops tile parameters) that overflows with perfectly legal run codes. This report is the **strip** variant — the callee-side missing bounds check plus T4's EOL-only row accounting and MH's write-before-validate. Fixing the caller mismatch does not address this vector; bounding `BitWriterUtils` addresses both (see remediation).\n\n**Suggested remediation:**\n1. Bound `BitWriterUtils.WriteBits/WriteBit/WriteZeroBit` against `buffer.Length*8` (return bool / throw `ImageFormatException`) — do not rely on caller discipline.\n2. In the T4/MH decompress loops, validate `(bitsWritten + RunLength) <= buffer.Length*8` **before** writing; abort T4 when accumulated rows exceed stripHeight instead of waiting for the loop to end naturally.\n3. In Modified Huffman, move the `pixelsWritten > Width` check before the actual write.\n4. Regression fuzz cases: Compression=2/3, EOL-less oversized makeup chains, edge widths; strip and tiled paths share the same constraint.\n\n## PoC\n\nFull PoC posted as the first comment below: `Program.cs` (driver), `poc-tiff-t4.tif` + `poc-tiff-mh.tif` (crafted files, ~90 KB each, base64 inline), README.\n\n1. Build a console project referencing `src/ImageSharp/ImageSharp.csproj`, run it against either crafted file (or call `Image.Load` on it).\n2. PoC layout: TIFF (II, 42), ImageWidth=64, ImageLength=1, BitsPerSample=1, Photometric=WhiteIsZero, RowsPerStrip=1; strip data = `EOL(12bit)` + 60000× `white makeup 2560 (000000011111)` + `white terminating code (000111)` — declared run ≈ 153,600,001 px ≈ 19.2 MB into an 8-byte buffer.\n3. Observed (both variants):\n\n ```\n strip payload 90003 bytes, claimed run ~153,600,001 px = 19,200,000 bytes into 8-byte buffer\n Fatal error. System.AccessViolationException: Attempted to read or write protected memory.\n at SixLabors.ImageSharp.Formats.Tiff.Compression.BitWriterUtils.WriteBits(Span`1<Byte>, IntPtr, IntPtr, Byte)\n at ...ModifiedHuffmanTiffCompression.Decompress(...)\n at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderCore.DecodeStripsChunky[Rgba32](...)\n Aborted (core dumped); exit=134\n ```\n\n The T4 variant crashes identically at `T4TiffCompression.WritePixelRun → BitWriterUtils.WriteBits`.\n4. Control: an equivalent file without the makeup chain (legal EOL-delimited rows) decodes normally — the crash comes from the oversized runs, not the container.\n\n## Impact\n\n- **What it is:** out-of-bounds write (CWE-787). For any service decoding untrusted TIFFs (image hosting/transcoding/thumbnails/CMS): reliable remote DoS (uncatchable fatal process crash), plus a heap OOB write with attacker-controlled offset and length — a realistic heap-corruption / potential code-execution surface. In scope of your SECURITY.md as a library vulnerability.\n- **Who is impacted:** applications decoding untrusted TIFF with SixLabors.ImageSharp at the current major (verified on main past v4.1.0); plain strip TIFFs with Compression=2/3 are the trigger, which ordinary TIFF writers can produce.\n\n---\n\n---\n\nReported by **Kimi Security Team** (bug-report@moonshot.ai).", | ||
| "details": "### Patched versions\n\nFixed in ImageSharp **3.2.0** and **4.1.1**. Users on v3 should upgrade to 3.2.0; users on v4 should upgrade to 4.1.1 or later.\n\n## Summary\n\nWhen decoding a fax-compressed strip TIFF (Compression=3 / Group 3 1D, or Compression=2 / Modified Huffman), the CCITT decompressors write decoded runs through `BitWriterUtils.WriteBits/WriteBit/WriteZeroBit`, which advance and write bits via `Unsafe.Add` with read-modify-write semantics — **without ever comparing the write position against the target buffer length**. The strip buffer is sized `ImageWidth × RowsPerStrip` (8 bytes in the PoC), but two independent defects let an attacker write tens of millions of bits past it: (a) T4 only increments `rowsWritten` when an EOL code is read, and one `ReadNextRun` accumulates unlimited makeup codes (+2560 px per 12-bit code) into a single `RunLength` that `WritePixelRun` then writes in one shot; (b) Modified Huffman writes **before** validating (`pixelsWritten > Width` is checked at :90-93, after the write at :56-63), so the overflow completes even though an exception is thrown later. One crafted ~90 KB file writes ~19.2 MB linearly past an 8-byte buffer and deterministically kills the process; the write offset and length are fully attacker-controlled (classic heap-corruption primitive on the managed heap).\n\nVerified at commit `5cd4d0d26a82a9549f297a237aea9cf665bddff8` (main; latest release v4.1.0, the supported major). A related tiled-path variant (tile-buffer width mismatch) was reported separately as GHSA-v76p-62qx-wwq2 — this report covers the distinct strip-path root cause.\n\n## Details\n\nRoot cause: the bit-writing sink has no bounds check, and neither decompressor constrains run lengths against the strip buffer.\n\n- Unchecked write primitive: [BitWriterUtils.cs#L51](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/BitWriterUtils.cs#L51) (`WriteBit`), :58 (`WriteZeroBit` — also read-modify-write, so even all-white runs really write), :11 (`WriteBits`)\n- T4 trigger: [T4TiffCompression.cs#L75](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/Decompressors/T4TiffCompression.cs#L75) and L109-L119 — `rowsWritten` only increments on EOL; consecutive makeup codes accumulate into one `RunLength`, then `WritePixelRun` writes it in full\n- MH trigger: [ModifiedHuffmanTiffCompression.cs#L56-L63](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/Compression/Decompressors/ModifiedHuffmanTiffCompression.cs#L56-L63) — write happens before the width check at L90-L93\n- Buffer size: [TiffDecoderCore.cs#L932-L967](https://github.com/SixLabors/ImageSharp/blob/5cd4d0d26a82a9549f297a237aea9cf665bddff8/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs#L932-L967) — `CalculateStripBufferSize` = width × bpp/8 × rowsPerStrip\n\nAttack surface: `Image.Load(stream)` on an attacker-supplied strip TIFF (`TiffDecoderCore.DecodeStripsChunky` → `TiffDecompressorsFactory.Create` → `Decompress`). Default configuration, no authentication, no user interaction — a plain strip TIFF (far more common than the tiled variant) with Compression=2 or 3 and a chain of CCITT makeup codes.\n\nRelationship to GHSA-v76p-62qx-wwq2: that report is the **tiled** variant — a caller-side width mismatch (`TiffDecompressorsFactory` drops tile parameters) that overflows with perfectly legal run codes. This report is the **strip** variant — the callee-side missing bounds check plus T4's EOL-only row accounting and MH's write-before-validate. Fixing the caller mismatch does not address this vector; bounding `BitWriterUtils` addresses both (see remediation).\n\n**Suggested remediation:**\n1. Bound `BitWriterUtils.WriteBits/WriteBit/WriteZeroBit` against `buffer.Length*8` (return bool / throw `ImageFormatException`) — do not rely on caller discipline.\n2. In the T4/MH decompress loops, validate `(bitsWritten + RunLength) <= buffer.Length*8` **before** writing; abort T4 when accumulated rows exceed stripHeight instead of waiting for the loop to end naturally.\n3. In Modified Huffman, move the `pixelsWritten > Width` check before the actual write.\n4. Regression fuzz cases: Compression=2/3, EOL-less oversized makeup chains, edge widths; strip and tiled paths share the same constraint.\n\n## PoC\n\nFull PoC posted as the first comment below: `Program.cs` (driver), `poc-tiff-t4.tif` + `poc-tiff-mh.tif` (crafted files, ~90 KB each, base64 inline), README.\n\n1. Build a console project referencing `src/ImageSharp/ImageSharp.csproj`, run it against either crafted file (or call `Image.Load` on it).\n2. PoC layout: TIFF (II, 42), ImageWidth=64, ImageLength=1, BitsPerSample=1, Photometric=WhiteIsZero, RowsPerStrip=1; strip data = `EOL(12bit)` + 60000× `white makeup 2560 (000000011111)` + `white terminating code (000111)` — declared run ≈ 153,600,001 px ≈ 19.2 MB into an 8-byte buffer.\n3. Observed (both variants):\n\n ```\n strip payload 90003 bytes, claimed run ~153,600,001 px = 19,200,000 bytes into 8-byte buffer\n Fatal error. System.AccessViolationException: Attempted to read or write protected memory.\n at SixLabors.ImageSharp.Formats.Tiff.Compression.BitWriterUtils.WriteBits(Span`1<Byte>, IntPtr, IntPtr, Byte)\n at ...ModifiedHuffmanTiffCompression.Decompress(...)\n at SixLabors.ImageSharp.Formats.Tiff.TiffDecoderCore.DecodeStripsChunky[Rgba32](...)\n Aborted (core dumped); exit=134\n ```\n\n The T4 variant crashes identically at `T4TiffCompression.WritePixelRun → BitWriterUtils.WriteBits`.\n4. Control: an equivalent file without the makeup chain (legal EOL-delimited rows) decodes normally — the crash comes from the oversized runs, not the container.\n\n## Impact\n\n- **What it is:** out-of-bounds write (CWE-787). For any service decoding untrusted TIFFs (image hosting/transcoding/thumbnails/CMS): reliable remote DoS (uncatchable fatal process crash), plus a heap OOB write with attacker-controlled offset and length — a realistic heap-corruption / potential code-execution surface. In scope of your SECURITY.md as a library vulnerability.\n- **Who is impacted:** applications decoding untrusted TIFF with SixLabors.ImageSharp at the current major (verified on main past v4.1.0); plain strip TIFFs with Compression=2/3 are the trigger, which ordinary TIFF writers can produce.\n\n---\n\n---\n\nReported by **Kimi Security Team** (bug-report@moonshot.ai).", |


Updates
Comments
The fix has been backported and released in ImageSharp 3.2.0. Split the affected ranges to exclude the fixed v3 release while preserving the existing v4 fix. The repository advisory has already been updated. Correct the NuGet package name to SixLabors.ImageSharp.
Release: https://github.com/SixLabors/ImageSharp/releases/tag/v3.2.0
Repository advisory: GHSA-jj3q-cwqj-842r