{"id":"GHSA-m9fg-w9qh-67pc","kind":"vulnerability","aliases":["GHSA-m9fg-w9qh-67pc"],"related":[],"title":"sanitize_message() address regex is lowercase-only and does not match PIL's uppercase __repr__, leaking live heap addresses to unauthenticated clients","description":"### Summary\r\n\r\n`sanitize_message()` scrubs heap addresses from error messages with the regex `r\" at 0x[0-9a-f]+>\"` - lowercase only, no `IGNORECASE`. `PIL.Image.Image.__repr__` formats its address as `0x{id(self):X}` - uppercase. The scrubber structurally cannot match the one object class both parent advisories are about.\r\n\r\nA 564-byte PNG sent to an unauthenticated endpoint returns a live heap address of the running server process.\r\n\r\nThis is not a new hardening proposal. The CVE-2026-54236 advisory explicitly recommended widening this exact regex to `[0-9a-fA-F]`; three months on it is byte-identical.\r\n\r\n### Details\r\n\r\n#### The mismatch\r\n\r\n```python\r\n# the scrubber\r\nr\" at 0x[0-9a-f]+>\"\r\n\r\n# what PIL actually emits\r\n# PIL/Image.py — Image.__repr__\r\n\"<%s.%s image mode=%s size=%dx%d at 0x%X>\" % (..., id(self))\r\n```\r\n\r\nThe parent CVE's own lowercase `BytesIO` shape is scrubbed correctly today. PIL's uppercase form is not.\r\n\r\n#### The sink\r\n\r\n`vllm/multimodal/processing/context.py:347` f-strings the PIL objects into any HF-processor failure message, which is returned to the client.\r\n\r\n#### Why the recommendation was dropped - the in-tree answer\r\n\r\n`test_error_sanitization.py` already contains a case with the id `pil-image-repr`, asserting that the PIL form is scrubbed. Its input is a hand-written **lowercase** literal that PIL never emits. I ran both halves against the same object class: the test passes, and a real `repr()` leaks.\r\n\r\nThat is why a documented, accepted recommendation looked already-covered and was never implemented. Any fix must include a test built from a real `repr()`, not from a literal.\r\n\r\n#### Related PRs inspected at diff level (neither closes this)\r\n\r\n* **PR #35492** adds `sanitize_message` to a new Responses-API WebSocket path, continuing the CVE-2026-54236 remediation - but does not widen the regex, and its new test also uses a lowercase literal.\r\n* **PR #52120** patches a stale path (`serve/utils/api_utils.py`) and only touches the path regex's backtracking.\r\n\r\n### Reachability - default configuration, no authentication\r\n\r\n* `api_key` defaults to `None`, and `AuthenticationMiddleware` is installed only when a key is configured - a default `vllm serve` is entirely unauthenticated.\r\n* Even with `--api-key` set, `GUARDED_PREFIX = (\"/v1\", \"/v2\", \"/inference\", \"/cohere\")`, so `/tokenize` remains unauthenticated.\r\n* Default-on for any multimodal deployment.\r\n\r\n### Impact\r\n\r\nInformation disclosure: a live heap address of the API-server process, to an unauthenticated remote client. CWE-532.\r\n\r\n**Scope, stated plainly.** This is information disclosure, not memory corruption. It is Stage 1 of the chain CVE-2026-22778 was rated 9.8 for - an ASLR defeat that made the second stage reliable. **Stage 2 is patched at the current OpenCV floor, so I am not claiming RCE.** The value of the fix is that the disclosure primitive the chain depended on is still live, and the advisory that recommended closing it was never acted on.\r\n\r\n**Proposed CVSS v3.1:** `AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N` (5.3, Medium). Severity is proposed, not insisted on.\r\n\r\n### Suggested fix\r\n\r\n1. Widen the regex to `[0-9a-fA-F]` and add `re.IGNORECASE`, as CVE-2026-54236's advisory recommended.\r\n2. Replace the `pil-image-repr` test case's hand-written literal with a string produced by calling `repr()` on a real `PIL.Image.Image`, so the case cannot pass while the production path leaks. Apply the same to PR #35492's new test.\r\n\r\nItem 2 matters as much as item 1 here: the test is the reason the first recommendation was lost.\r\n\r\n### Weaknesses of this report, stated upfront\r\n\r\n* No GPU was used. Results come from the in-tree GPU-less render server (`vllm launch render`), which runs the real frontend, the real request model and the real `/tokenize` route. Media processing happens entirely in the frontend, so I do not believe the engine's presence changes the result - but I have not confirmed it under a full `vllm serve`.\r\n* The disclosed value is an address, not memory contents. No read primitive is claimed.\r\n* I did not attempt to chain this to anything.\r\n\r\n---\r\n\r\n*Prepared with AI assistance (Claude), per the repository's contributing guidance on disclosing AI-assisted contributions. All findings were verified by executing vLLM's own code at the commits cited; the analysis and the claims are my own.*\r\n\r\n*Reported by Eva Crystal / 0xiviel (XSource Security).*\r\n\r\n## Affected versions\r\n\r\n- Package: `vllm` (PyPI).\r\n- Affected versions: `>= 0.14.0rc1, < 0.31.0`.\r\n- First stable patched version: `0.31.0`.\r\n\r\nAdvisory fixed in [#58832](https://github.com/vllm-project/vllm/pull/58832).","summary":"vLLM's error-message scrubber only removes lowercase hexadecimal memory addresses, but Pillow's image repr prints them in uppercase, so a crafted small PNG sent to an unauthenticated endpoint returns a live heap address of the server process. The attacker gets an address, not memory contents, which weakens ASLR protection for a later exploit chain.","severity":"medium","cvss":5.3,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"vllm","fixed":["0.31.0"],"affected":">= 0.14.0rc1, < 0.31.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["ai-tooling","cwe-532","multimodal","python","self-hosted","vllm"],"not_affected_if":["no-multimodal-inputs","not-network-exposed","version-0.31.0-or-later"],"how_to_check":"- Check the installed vLLM version with `pip show vllm` and treat versions `>= 0.14.0rc1` and `< 0.31.0` as affected.\n- Check whether multimodal inputs are served, since the sink is media processing in the frontend.\n- Check whether `--api-key` is set; the advisory says `/tokenize` stays unauthenticated even when it is.","fix":"- `vllm`: upgrade to `0.31.0` or later.","status":"open","published_at":"2026-10-27T22:14:15+00:00","first_seen_at":"2026-11-09T22:53:23+00:00","triaged_at":"2026-11-09T22:56:44+00:00","updated_at":"2026-11-09T22:56:44+00:00","category":"ai-tooling","confidence":0.8,"triage":{"at":"2026-11-09T22:56:44Z","model":"claude-haiku-5-5","output":{"fix":"- `vllm`: upgrade to `0.31.0` or later.","reach":"internet","title":"vLLM: error scrubber misses uppercase PIL addresses, leaking heap address to clients","impact":"low","summary":"vLLM's error-message scrubber only removes lowercase hexadecimal memory addresses, but Pillow's image repr prints them in uppercase, so a crafted small PNG sent to an unauthenticated endpoint returns a live heap address of the server process. The attacker gets an address, not memory contents, which weakens ASLR protection for a later exploit chain.","audience":"businesses","category":"ai-tooling","nicknames":[],"who_cares":["vllm","python","self-hosted","multimodal"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"unauthenticated client learns a live heap address of the API server","analyst_note":"","how_to_check":"- Check the installed vLLM version with `pip show vllm` and treat versions `>= 0.14.0rc1` and `< 0.31.0` as affected.\n- Check whether multimodal inputs are served, since the sink is media processing in the frontend.\n- Check whether `--api-key` is set; the advisory says `/tokenize` stays unauthenticated even when it is.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["version-0.31.0-or-later","no-multimodal-inputs","not-network-exposed"],"who_is_affected":"Teams running vLLM 0.14.0rc1 up to but not including 0.31.0 with multimodal (image) inputs enabled. The advisory states a default `vllm serve` has no authentication, and `/tokenize` stays reachable even with `--api-key` set, so exposure on a network-reachable server is the main concern. Teams on 0.31.0 or later, or on versions below 0.14.0rc1, are not affected.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"medium"},"cost_usd":0.000983,"input_tokens":6321,"output_tokens":701,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-2026':180C,392C,513C,609C '-22778':514C '-532':39B,494C '-54236':181C,393C,610C '-9':119C,191C,214C,599C '/cohere':463C '/inference':462C '/tokenize':465C,714C '/v1':460C '/v2':461C '0':118C,190C,213C,598C '0x':117C,133C,212C,233C '1':508C,593C,665C '2':529C,614C,659C '347':258C '35492':377C,654C '5.3':583C '52120':411C '564':153C '67pc':28A,33A '9.8':517C 'a-f':120C,215C 'a-fa-f':192C,600C 'accept':342C 'act':569C 'actual':220C 'add':378C,605C 'addr':751C 'address':3A,20A,57B,81B,90B,108C,131C,165C,481C 'advisori':149C,182C,562C,612C 'ai':36B 'ai-tool':35B 'alreadi':289C,346C 'already-cov':345C 'also':405C 'answer':287C 'api':386C,430C,455C,485C 'api-key':454C 'api-serv':484C 'appli':649C 'aslr':96B,520C 'assert':300C 'attack':87B 'authent':429C 'authenticationmiddlewar':436C 'av':574C 'backtrack':424C 'believ':727C 'built':358C 'byte':154C,202C 'byte-ident':201C 'bytesio':243C 'call':633C 'cannot':141C,642C 'case':292C,622C,641C 'chain':102B,511C,554C 'chang':732C 'claim':541C 'class':146C,328C 'client':23A,277C,492C 'close':374C,565C 'come':688C 'configur':427C,444C 'confirm':739C 'contain':290C 'content':93B 'continu':389C 'correct':247C 'corrupt':504C 'cover':347C 'craft':69B 'current':534C 'cve':179C,239C,391C,512C,608C 'cvss':572C 'cwe':38B,493C 'd':231C 'default':426C,432C,446C,469C 'default-on':468C 'defeat':521C 'depend':555C 'deploy':474C 'detail':204C 'diff':371C 'disclos':747C 'disclosur':477C,501C,551C 'document':341C 'drop':282C 'dx':230C 'emit':221C,319C 'endpoint':76B,160C 'engin':729C 'entir':450C,719C 'error':49B,110C 'error-messag':48B 'even':452C 'exact':187C 'explicit':183C 'exploit':101B 'f':122C,195C,217C,260C,603C 'f-string':259C 'fa':194C,602C 'failur':270C 'first':673C 'fix':353C,547C,592C 'floor':536C 'form':252C,304C 'format':129C 'frontend':706C,722C 'full':743C 'get':88B 'ghsa':25A,30A 'ghsa-m9fg-w9qh-67pc':24A,29A 'gpu':684C,695C 'gpu-less':694C 'guard':458C 'halv':323C 'hand':312C,625C 'hand-written':311C,624C 'happen':718C 'harden':176C 'heap':19A,80B,107C,164C,480C 'hexadecim':55B 'hf':268C 'hf-processor':267C 'host':44B 'id':134C,235C,295C 'ident':203C 'ignorecas':126C 'imag':61B,226C,298C,619C 'image.__repr__':223C 'impact':475C 'implement':351C 'in-tre':284C,691C 'includ':355C 'inform':476C,500C 'input':308C 'insist':589C 'inspect':369C 'instal':438C 'item':658C,664C 'key':431C,442C,456C 'l/i':580C 'l/pr':576C 'later':100B 'launch':700C 'leak':17A,336C,648C 'less':696C 'level':372C 'liter':315C,366C,409C,627C 'live':18A,79B,163C,479C,559C 'look':344C 'lost':676C 'lowercas':7A,54B,123C,242C,314C,408C 'lowercase-on':6A 'm9fg':26A,31A 'made':523C 'match':12A,142C 'matter':660C 'media':716C 'medium':584C 'memori':56B,92B,503C 'messag':2A,50B,105C,111C,271C,380C 'mismatch':206C 'mode':227C 'model':710C 'month':197C 'much':662C 'multimod':40B,473C 'must':354C 'n':582C 'n/a':581C 'n/ac':575C 'n/s':578C 'n/ui':577C 'neither':373C 'never':318C,350C,568C 'new':175C,383C,403C,656C 'none':434C 'object':145C,264C,327C 'one':144C 'opencv':535C 'parent':148C,238C 'pass':331C,643C 'patch':412C,531C 'path':388C,415C,421C,647C 'pil':13A,219C,249C,263C,297C,303C,317C,618C 'pil-image-repr':296C,617C 'pil.image.image':127C,638C 'pil/image.py':222C 'pillow':59B 'plain':497C 'png':71B,155C 'pr':376C,410C,653C 'prefix':459C 'presenc':731C 'primit':552C 'print':63B 'process':85B,170C,487C,717C 'processor':269C 'produc':631C 'product':646C 'propos':177C,571C,587C 'protect':97B 'prs':368C 'python':41B,207C 'r':115C,210C 'ran':321C 'rate':516C 'rce':542C 're.ignorecase':606C 'reachabl':425C 'real':334C,361C,637C,705C,708C,713C 'reason':671C 'recommend':184C,280C,343C,564C,613C,674C 'regex':4A,114C,188C,400C,422C,596C 'relat':367C 'reliabl':527C 'remain':466C 'remedi':394C 'remot':491C 'remov':53B 'render':697C,701C 'replac':615C 'report':680C 'repr':16A,62B,128C,299C,335C,362C,620C,634C 'request':709C 'respons':385C 'responses-api':384C 'result':687C,734C 'return':77B,161C,274C 'rout':715C 'run':168C,703C 'sanit':1A,104C,379C 'scope':495C 'scrub':106C,246C,306C 'scrubber':51B,139C,209C 'second':525C 'self':43B,135C,236C 'self-host':42B 'sent':72B,156C 'serv':448C,745C 'serve/utils/api_utils.py':416C 'server':84B,169C,486C,698C 'set':457C 'sever':585C 'shape':244C 'sink':256C 'size':229C 'small':70B 'stage':507C,526C,528C 'stale':414C 'state':496C,681C 'still':558C 'string':261C,630C 'structur':140C 'suggest':591C 'summari':103C 'test':330C,357C,404C,621C,657C,668C 'test_error_sanitization.py':288C 'three':196C 'today':248C 'tool':37B 'touch':419C 'tree':286C,693C 'u/c':579C 'unauthent':22A,75B,159C,451C,467C,490C 'upfront':682C 'uppercas':15A,66B,137C,251C 'use':406C,686C 'v3.1':573C 'valu':544C,748C 'vllm':34A,45B,46B,447C,699C,744C 'vllm/multimodal/processing/context.py':257C 'w9qh':27A,32A 'weak':677C 'weaken':95B 'websocket':387C 'widen':185C,398C,594C 'written':313C,626C 'x':136C,234C","nicknames":[],"mitigations":[],"who_affected":"Teams running vLLM 0.14.0rc1 up to but not including 0.31.0 with multimodal (image) inputs enabled. The advisory states a default `vllm serve` has no authentication, and `/tokenize` stays reachable even with `--api-key` set, so exposure on a network-reachable server is the main concern. Teams on 0.31.0 or later, or on versions below 0.14.0rc1, are not affected.","tier":"B","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["vllm"],"ptokens":["vllm"],"labels":["aitooling","cwe532","medium","multimodal","pypi","python","selfhosted","vllm"],"campaign_type":null,"search_a":"'67pc':28A,33A 'address':3A,20A 'client':23A 'ghsa':25A,30A 'ghsa-m9fg-w9qh-67pc':24A,29A 'heap':19A 'leak':17A 'live':18A 'lowercas':7A 'lowercase-on':6A 'm9fg':26A,31A 'match':12A 'messag':2A 'pil':13A 'regex':4A 'repr':16A 'sanit':1A 'unauthent':22A 'uppercas':15A 'vllm':34A 'w9qh':27A,32A","search_b":"'-532':39B 'address':57B,81B,90B 'ai':36B 'ai-tool':35B 'aslr':96B 'attack':87B 'chain':102B 'content':93B 'craft':69B 'cwe':38B 'endpoint':76B 'error':49B 'error-messag':48B 'exploit':101B 'get':88B 'heap':80B 'hexadecim':55B 'host':44B 'imag':61B 'later':100B 'live':79B 'lowercas':54B 'memori':56B,92B 'messag':50B 'multimod':40B 'pillow':59B 'png':71B 'print':63B 'process':85B 'protect':97B 'python':41B 'remov':53B 'repr':62B 'return':77B 'scrubber':51B 'self':43B 'self-host':42B 'sent':72B 'server':84B 'small':70B 'tool':37B 'unauthent':75B 'uppercas':66B 'vllm':45B,46B 'weaken':95B","search_c":"'-2026':180C,392C,513C,609C '-22778':514C '-532':494C '-54236':181C,393C,610C '-9':119C,191C,214C,599C '/cohere':463C '/inference':462C '/tokenize':465C,714C '/v1':460C '/v2':461C '0':118C,190C,213C,598C '0x':117C,133C,212C,233C '1':508C,593C,665C '2':529C,614C,659C '347':258C '35492':377C,654C '5.3':583C '52120':411C '564':153C '9.8':517C 'a-f':120C,215C 'a-fa-f':192C,600C 'accept':342C 'act':569C 'actual':220C 'add':378C,605C 'addr':751C 'address':108C,131C,165C,481C 'advisori':149C,182C,562C,612C 'alreadi':289C,346C 'already-cov':345C 'also':405C 'answer':287C 'api':386C,430C,455C,485C 'api-key':454C 'api-serv':484C 'appli':649C 'aslr':520C 'assert':300C 'authent':429C 'authenticationmiddlewar':436C 'av':574C 'backtrack':424C 'believ':727C 'built':358C 'byte':154C,202C 'byte-ident':201C 'bytesio':243C 'call':633C 'cannot':141C,642C 'case':292C,622C,641C 'chain':511C,554C 'chang':732C 'claim':541C 'class':146C,328C 'client':277C,492C 'close':374C,565C 'come':688C 'configur':427C,444C 'confirm':739C 'contain':290C 'continu':389C 'correct':247C 'corrupt':504C 'cover':347C 'current':534C 'cve':179C,239C,391C,512C,608C 'cvss':572C 'cwe':493C 'd':231C 'default':426C,432C,446C,469C 'default-on':468C 'defeat':521C 'depend':555C 'deploy':474C 'detail':204C 'diff':371C 'disclos':747C 'disclosur':477C,501C,551C 'document':341C 'drop':282C 'dx':230C 'emit':221C,319C 'endpoint':160C 'engin':729C 'entir':450C,719C 'error':110C 'even':452C 'exact':187C 'explicit':183C 'f':122C,195C,217C,260C,603C 'f-string':259C 'fa':194C,602C 'failur':270C 'first':673C 'fix':353C,547C,592C 'floor':536C 'form':252C,304C 'format':129C 'frontend':706C,722C 'full':743C 'gpu':684C,695C 'gpu-less':694C 'guard':458C 'halv':323C 'hand':312C,625C 'hand-written':311C,624C 'happen':718C 'harden':176C 'heap':107C,164C,480C 'hf':268C 'hf-processor':267C 'id':134C,235C,295C 'ident':203C 'ignorecas':126C 'imag':226C,298C,619C 'image.__repr__':223C 'impact':475C 'implement':351C 'in-tre':284C,691C 'includ':355C 'inform':476C,500C 'input':308C 'insist':589C 'inspect':369C 'instal':438C 'item':658C,664C 'key':431C,442C,456C 'l/i':580C 'l/pr':576C 'launch':700C 'leak':336C,648C 'less':696C 'level':372C 'liter':315C,366C,409C,627C 'live':163C,479C,559C 'look':344C 'lost':676C 'lowercas':123C,242C,314C,408C 'made':523C 'match':142C 'matter':660C 'media':716C 'medium':584C 'memori':503C 'messag':105C,111C,271C,380C 'mismatch':206C 'mode':227C 'model':710C 'month':197C 'much':662C 'multimod':473C 'must':354C 'n':582C 'n/a':581C 'n/ac':575C 'n/s':578C 'n/ui':577C 'neither':373C 'never':318C,350C,568C 'new':175C,383C,403C,656C 'none':434C 'object':145C,264C,327C 'one':144C 'opencv':535C 'parent':148C,238C 'pass':331C,643C 'patch':412C,531C 'path':388C,415C,421C,647C 'pil':219C,249C,263C,297C,303C,317C,618C 'pil-image-repr':296C,617C 'pil.image.image':127C,638C 'pil/image.py':222C 'plain':497C 'png':155C 'pr':376C,410C,653C 'prefix':459C 'presenc':731C 'primit':552C 'process':170C,487C,717C 'processor':269C 'produc':631C 'product':646C 'propos':177C,571C,587C 'prs':368C 'python':207C 'r':115C,210C 'ran':321C 'rate':516C 'rce':542C 're.ignorecase':606C 'reachabl':425C 'real':334C,361C,637C,705C,708C,713C 'reason':671C 'recommend':184C,280C,343C,564C,613C,674C 'regex':114C,188C,400C,422C,596C 'relat':367C 'reliabl':527C 'remain':466C 'remedi':394C 'remot':491C 'render':697C,701C 'replac':615C 'report':680C 'repr':128C,299C,335C,362C,620C,634C 'request':709C 'respons':385C 'responses-api':384C 'result':687C,734C 'return':161C,274C 'rout':715C 'run':168C,703C 'sanit':104C,379C 'scope':495C 'scrub':106C,246C,306C 'scrubber':139C,209C 'second':525C 'self':135C,236C 'sent':156C 'serv':448C,745C 'serve/utils/api_utils.py':416C 'server':169C,486C,698C 'set':457C 'sever':585C 'shape':244C 'sink':256C 'size':229C 'stage':507C,526C,528C 'stale':414C 'state':496C,681C 'still':558C 'string':261C,630C 'structur':140C 'suggest':591C 'summari':103C 'test':330C,357C,404C,621C,657C,668C 'test_error_sanitization.py':288C 'three':196C 'today':248C 'touch':419C 'tree':286C,693C 'u/c':579C 'unauthent':159C,451C,467C,490C 'upfront':682C 'uppercas':137C,251C 'use':406C,686C 'v3.1':573C 'valu':544C,748C 'vllm':447C,699C,744C 'vllm/multimodal/processing/context.py':257C 'weak':677C 'websocket':387C 'widen':185C,398C,594C 'written':313C,626C 'x':136C,234C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-27T22:14:15+00:00","kev":null,"cvss_vector":null,"cvss_by":null,"poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-m9fg-w9qh-67pc","url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-m9fg-w9qh-67pc","severity":"medium","published_at":"2026-10-27T22:14:15+00:00","modified_at":"2026-10-27T22:14:15+00:00","first_seen_at":"2026-11-09T22:53:23+00:00","fetched_at":"2026-11-09T22:53:23+00:00","note":null}],"documents":[{"id":110587,"url":"https://github.com/vllm-project/vllm/security/advisories/GHSA-m9fg-w9qh-67pc","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}