{"id":"CVE-2026-34755","aliases":["GHSA-pq5c-rjhq-qp7p","PYSEC-2026-144"],"url":"https://o3.security/vulnerability/CVE-2026-34755","summary":"vLLM Affected by Denial of Service via Unbounded Frame Count in video/jpeg Base64 Processing","details":"## Summary\n\nThe `VideoMediaIO.load_base64()` method at `vllm/multimodal/media/video.py:51-62` splits `video/jpeg` data URLs by comma to extract individual JPEG frames, but does not enforce a frame count limit. The `num_frames` parameter (default: 32), which is enforced by the `load_bytes()` code path at line 47-48, is completely bypassed in the `video/jpeg` base64 path. An attacker can send a single API request containing thousands of comma-separated base64-encoded JPEG frames, causing the server to decode all frames into memory and crash with OOM.\n\n## Details\n\n### Vulnerable code\n\n```python\n# video.py:51-62\ndef load_base64(self, media_type: str, data: str) -> tuple[npt.NDArray, dict[str, Any]]:\n    if media_type.lower() == \"video/jpeg\":\n        load_frame = partial(self.image_io.load_base64, \"image/jpeg\")\n        return np.stack(\n            [np.asarray(load_frame(frame_data)) for frame_data in data.split(\",\")]\n            #                                                       ^^^^^^^^^^\n            # Unbounded split — no frame count limit\n        ), {}\n    return self.load_bytes(base64.b64decode(data))\n```\n\nThe `load_bytes()` path (line 47-48) properly delegates to a video loader that respects `self.num_frames` (default 32). The `load_base64(\"video/jpeg\", ...)` path bypasses this limit entirely — `data.split(\",\")` produces an unbounded list and every frame is decoded into a numpy array.\n\n### video/jpeg is part of vLLM's public API\n\n`video/jpeg` is a vLLM-specific MIME type, not IANA-registered. However it is part of the public API surface:\n\n- `encode_video_url()` at `vllm/multimodal/utils.py:96-108` generates `data:video/jpeg;base64,...` URLs\n- Official test suites at `tests/entrypoints/openai/test_video.py:62` and `tests/entrypoints/test_chat_utils.py:153` both use this format\n\n### Memory amplification\n\nEach JPEG frame decodes to a full numpy array. For 640x480 RGB images, each frame is ~921 KB decoded. 5000 frames = ~4.6 GB. `np.stack()` then creates an additional copy. The compressed JPEG payload is small (~100 KB for 5000 frames) but decompresses to gigabytes.\n\n### Data flow\n\n```\nPOST /v1/chat/completions\n  → chat_utils.py:1434   video_url type → mm_parser.parse_video()\n  → chat_utils.py:872    parse_video() → self._connector.fetch_video()\n  → connector.py:295     fetch_video() → load_from_url(url, self.video_io)\n  → connector.py:91      _load_data_url(): url_spec.path.split(\",\", 1)\n                          → media_type = \"video/jpeg\"\n                          → data = \"<frame1>,<frame2>,...,<frame10000>\"\n  → connector.py:100     media_io.load_base64(\"video/jpeg\", data)\n  → video.py:54          data.split(\",\")  ← UNBOUNDED\n  → video.py:55-57       all frames decoded into numpy arrays\n  → video.py:56          np.stack([...])  ← massive combined array → OOM\n```\n\n`connector.py:91` uses `split(\",\", 1)` which splits on only the first comma. All remaining commas stay in `data` and are later split by `video.py:54`.\n\n### Comparison with existing protections\n\n| Code Path | Frame Limit | File |\n|-----------|-------------|------|\n| `load_bytes()` (binary video) | Yes — `num_frames` (default 32) | video.py:46-49 |\n| `load_base64(\"video/jpeg\", ...)` | No — unlimited `data.split(\",\")` | video.py:51-62 |","published":"2026-04-06T15:38:53.201Z","modified":"2026-08-27T11:30:47.678503699Z","cvss":{"score":6.5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H"},"epss":{"score":0.00464,"percentile":0.39258,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"vllm","fixedVersion":"0.19.0"}],"fix":{"url":"https://github.com/vllm-project/vllm/pull/38636","label":"vllm-project/vllm#38636"},"references":[{"type":"WEB","url":"https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-34755.json"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:36005"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:36006"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:57380"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:57387"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:57389"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:57390"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:59144"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:59151"},{"type":"ADVISORY","url":"https://access.redhat.com/security/cve/CVE-2026-34755"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/34xxx/CVE-2026-34755.json"},{"type":"ADVISORY","url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-pq5c-rjhq-qp7p"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34755"},{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=2455403"},{"type":"WEB","url":"https://github.com/vllm-project/vllm/pull/38636"},{"type":"WEB","url":"https://github.com/vllm-project/vllm/commit/58ee61422169ce17e08248f8efa1e9df434fe395"},{"type":"PACKAGE","url":"https://github.com/vllm-project/vllm"},{"type":"WEB","url":"https://github.com/pypa/advisory-database/tree/main/vulns/vllm/PYSEC-2026-144.yaml"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-27T11:30:47.678503699Z"}}