{"id":"CVE-2026-112390","kind":"vulnerability","aliases":["CVE-2026-112390","GHSA-22hg-r758-rrw9"],"related":[],"title":"Pillow: Infinite loop in MIC n_frames when the embedded TIFF","description":"```\n============================================================================\nInfinite loop in MIC n_frames when the embedded TIFF has 3+ IFDs\nLocation: src/PIL/MicImagePlugin.py:86  in MicImageFile.tell()\nCommit:   a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nA crafted 5.6 KB image file makes Pillow loop forever at 100% CPU when an application asks how many frames the image has. This only affects installations that have the optional `olefile` package. Any application that opens an untrusted image and then reads `im.n_frames` can have one worker tied up indefinitely. That includes the common \"is this image animated?\" check and re-saving with `save_all=True` to TIFF, PDF or WebP. The call never returns, raises no exception, and does not grow memory, so only an external watchdog stops it.\n\nThe file is a Microsoft Image Composer (MIC) container, which Pillow recognises by its magic bytes with no extension needed. Inside it is a TIFF stream with three chained image directories. `MicImageFile` subclasses `TiffImageFile` and overrides `seek()` and `tell()` so that a \"frame\" means one stream in the container. It still inherits `TiffImageFile.n_frames` (`src/PIL/TiffImagePlugin.py:1212`), which walks the embedded TIFF's directory chain with `self._seek(self.tell() + 1)` until `_n_frames` is set. Two things stop that walk from making progress:\n\n- `MicImageFile.tell()` (`src/PIL/MicImagePlugin.py:86`) returns the constant stream index.\n- `MicImageFile.seek()` calls `TiffImageFile._open()`, which resets `_n_frames` to `None`.\n\nAs a result, the loop re-parses the second directory forever whenever the embedded stream has three or more directories. `Image.open()`, `load()`, `thumbnail()`, `convert()`, `is_animated` and `ImageSequence` iteration do not hang on this file. The regression was introduced in 0b33a87b0d15 (April 2020, first released in 7.2.0), which removed `MicImageFile`'s own `n_frames` property. The attached reproducer builds the file in code and checks two controls, which return 3 and 1 frames as expected; the malicious file is still running when it is killed after 20 seconds.\n\n## Details\n\n`MicImageFile` (registered for `Image.open` whenever `olefile` is installed) subclasses `TiffImageFile` and redefines a \"frame\" as one OLE `*.ACI/Image` stream, overriding `seek()` and `tell()` accordingly. It does not override the `n_frames` property, so the inherited TIFF implementation runs, and that implementation relies on `tell()` advancing as IFDs are loaded. `MicImageFile.tell()` returns the OLE stream index, which never changes inside `n_frames`, so the loop re-parses the same IFD forever.\n\nA 5632-byte OLE2 compound file whose `img0.ACI/Image` stream is a 351-byte TIFF with three chained 1×1 IFDs is enough. `Image.open()` on it succeeds (format `MIC`, detected by the OLE magic, no extension needed); evaluating `im.n_frames` then never returns and keeps one core at 100 %. A Python stack taken 27 s in is\n\n```\n  File \"src/PIL/TiffImagePlugin.py\", line 1622 in _setup\n  File \"src/PIL/TiffImagePlugin.py\", line 1271 in _seek\n  File \"src/PIL/TiffImagePlugin.py\", line 1219 in n_frames\n```\n\nand instrumenting `_seek` shows it being called with `frame=1`, `tell()==0`, `_frame_pos==[8, 122]`, `_n_frames is None` on every iteration — identical state, so the loop is non-terminating, not merely slow. The same 351 TIFF bytes opened directly give `n_frames == 3` in milliseconds, and the same container around a 1-IFD TIFF gives `n_frames == 1`.\n\nThe loop, `src/PIL/TiffImagePlugin.py:1212`:\n\n```python\n    @property\n    def n_frames(self) -> int:\n        current_n_frames = self._n_frames\n        if current_n_frames is None:\n            current = self.tell()\n            self._seek(len(self._frame_pos))\n            while self._n_frames is None:\n                self._seek(self.tell() + 1)      # 1219\n            self.seek(current)\n```\n\nand the overrides that break its progress assumption, `src/PIL/MicImagePlugin.py:76`:\n\n```python\n    def seek(self, frame: int) -> None:\n        …\n        self.fp = self.ole.openstream(filename)\n        TiffImagePlugin.TiffImageFile._open(self)   # 82: resets _n_frames to None\n        self.frame = frame\n\n    def tell(self) -> int:\n        return self.frame                            # 87: OLE stream index, not TIFF __frame\n```\n\nFor a plain TIFF, `_seek(k)` (`TiffImagePlugin.py:1236`) loads IFDs up to `k`, appending each offset to `_frame_pos`, setting `_n_frames` when an IFD's next-pointer is 0 (or already seen), and setting the private `__frame = k`; `TiffImageFile.tell()` returns `__frame`, so `self.tell() + 1` walks the chain to its end.\n\nFor a MIC file, `Image.open` → `MicImageFile._open` sets `self._n_frames = len(self.images)` at line 69, but then calls `self.seek(0)` → `TiffImageFile._open` (line 82), which unconditionally sets `self._n_frames = None` (`TiffImagePlugin.py:1203`) and `_seek(0)`s into the embedded stream. With the 3-IFD stream, IFD 0 has a non-zero next pointer, so `_n_frames` stays `None` after open. `n_frames` then runs: `current = self.tell()` is `0` (the OLE index); `_seek(len(_frame_pos))` = `_seek(1)` loads IFD 1, whose next pointer is also non-zero and not yet in `_frame_pos`, so `_n_frames` is still `None`. The `while` body computes `self.tell() + 1` = `0 + 1` and calls `_seek(1)` again; `len(_frame_pos)` is already 2, so nothing new is loaded — IFD 1 is re-read via `tag_v2.load` and `_setup()` and the state is unchanged. This repeats indefinitely; IFD 2 is never reached. Two chained IFDs are not enough (IFD 1's next pointer of 0 sets `_n_frames = 2` on the first `_seek(1)`); three or more are.\n\n`Tests/test_file_mic.py::test_n_frames` passes only because `hopper.mic`'s stream has a single IFD, so `_seek(0)` sets `_n_frames = 1` and the loop is never entered; the `len(self.images)` assignment at line 69 is only consumed by the initial `_seek_check(0)` and is overwritten before any caller can read `n_frames`.\n\nA side effect of the same mismatch: with a 2-IFD stream `n_frames` returns 2 (TIFF IFDs) while `seek(1)` indexes `self.images[1]` (OLE streams) and raises `IndexError` — visible with `save_all=True`.\n\n## Impact\n\nAny application that lets Pillow open an uploaded file and then reads `im.n_frames` — the usual way to decide whether an image is animated (`getattr(im, \"n_frames\", 1) > 1`), and also what `hasattr(im, \"n_frames\")`/`getattr(im, \"n_frames\", 1)` in Pillow's own `TiffImagePlugin._save_all` (`:2333`), `PdfImagePlugin._save` (`:250`) and `WebPImagePlugin._save_all` (`:171`) evaluate when the app re-saves with `save_all=True` — can be made to spin one worker at 100 % CPU indefinitely with a 5.6 KB upload. The file needs no particular extension (OLE magic; `FpxImagePlugin` rejects it on CLSID and `MicImagePlugin` accepts it), default limits do not intervene, memory does not grow, and no exception is raised, so only an external watchdog ends it. Preconditions: `olefile` installed (it is an optional dependency; without it the MIC plugin is not registered) and the application touching `n_frames` or a `save_all` path. `Image.open()` itself, `load()`, `thumbnail()`, `convert()`, `is_animated` and `ImageSequence` iteration do not hang on this file. This is the same class of tiny-file, pure-Python non-termination as CVE-2021-28676 (FLI) and CVE-2021-28677 (EPS), and like CVE-2019-19911 (FPX) it depends on `olefile`; it differs in not being reached by the default open/load path. The regression was introduced in `0b33a87b0d15` (\"Replaced property methods for n_frames and is_animated with normal properties\", 2020-04-13), first released in 7.2.0, which replaced `MicImageFile`'s own `n_frames` property with the `_n_frames` attribute that `TiffImageFile._open` overwrites.\n\n## Reproducing\n\n`repro` is a single Python script that runs against the installed Pillow (with `olefile`, which is what registers the MIC plugin). It builds every input in code, writes each one to a temporary file named `x` with no extension (so Pillow identifies the format from its magic bytes), and opens it in a child Python process that runs `Image.open(path)` and prints `im.format` and `im.n_frames`.\n\nIt builds two things in code:\n\n- `tiff(n)`: an `n`-page TIFF of 4×4 greyscale frames written by Pillow itself (`save_all=True`), so the IFDs are chained exactly as Pillow's own writer chains them. The last next-IFD pointer is 0.\n- `mic(stream)`: a minimal OLE2 compound file with a storage named `img0.ACI` that contains a stream named `Image`, which is the layout `MicImageFile._open` looks for. Sector 0 is the FAT and sector 1 is the directory. The stream starts at sector 2 and is padded to 4096 bytes, so the file doesn't need a mini-FAT. The result is 5632 bytes.\n\nIt runs three inputs, each in its own child process with a hard 20-second kill:\n\n1. control: the plain 3-page TIFF. Expected `TIFF n_frames = 3`.\n2. control: a MIC wrapping a 1-page TIFF. Expected `MIC n_frames = 1`.\n3. attack: the same MIC container wrapping the 3-page TIFF.\n\nThe timeout is enforced by killing the child rather than by an in-process alarm, because a `TimeoutError` raised while `ImageFileDirectory_v2.load()` is parsing would be caught by its `except OSError` (`TiffImagePlugin.py:958`) and leave the IFD half-parsed instead of stopping the loop.\n\nThe script prints `BUG` when both controls return and the attack child is still running after 20 seconds; otherwise it prints `OK`. With the attached patch applied, the attack case prints `MIC n_frames = 1` and the script prints `OK`.\n\n## Suggested fix\n\nThe attached patch restores the OLE-level frame count after the embedded TIFF stream has been opened, so the inherited `n_frames` never enters its IFD-walking loop and `n_frames`/`seek()` agree on what a frame is. In `MicImageFile.seek`, `src/PIL/MicImagePlugin.py:82`, right after `TiffImagePlugin.TiffImageFile._open(self)`:\n\n```diff\n         TiffImagePlugin.TiffImageFile._open(self)\n \n+        # TiffImageFile._open() resets the frame count to that of the\n+        # embedded TIFF stream, but the frames of an MIC file are its streams\n+        self._n_frames = len(self.images)\n+        self.is_animated = self._n_frames > 1\n+\n         self.frame = frame\n```\n\nDoing this in `seek()` restores the invariant on every seek, not only the first. The existing assignments at lines 69–70 of `_open` are kept: the initial `seek(0)` → `_seek_check(0)` reads `n_frames` before `TiffImageFile._open` has run, so they are still needed. Plain read-only `n_frames`/`is_animated` properties, as before 0b33a87b0, are no longer a drop-in option because `TiffImageFile._seek` assigns `self.is_animated`. With the patch applied to a clean tree and rebuilt, the attack file returns `n_frames == 1` immediately, a 2-IFD stream raises `EOFError` on `seek(1)` instead of `IndexError`, `save_all=True` to TIFF and PDF completes, and `Tests/test_file_mic.py` and `Tests/test_file_tiff.py` pass. No regression test is included, since one would need a new binary MIC fixture in `Tests/images`.\n\nThe patch passed that check but has not been reviewed beyond it as the final form of the fix. Two things it leaves alone: it does not add the `_im` reset that `TiffImageFile.seek` performs after `_seek` (`TiffImagePlugin.py:1229`), which matters when switching between OLE streams of different size or mode; and `TiffImageFile.n_frames` still advances via the overridable `tell()` rather than its private `__frame`, so making it iterate on `__frame` would protect any other subclass that redefines `tell()`.\n\n## The patch\n\nRestore the OLE stream frame count in MicImageFile.seek() after TiffImageFile._open() resets it, so n_frames cannot loop forever\n\nApplies to `src/PIL/MicImagePlugin.py` at the commit above. It passed the mechanical check — applied to a clean tree, rebuilt, the reproducer stopped firing — but a reviewer did not endorse it as the right fix.\n\n```diff\ndiff --git a/src/PIL/MicImagePlugin.py b/src/PIL/MicImagePlugin.py\nindex f8cf3a76f..2bb0109ce 100644\n--- a/src/PIL/MicImagePlugin.py\n+++ b/src/PIL/MicImagePlugin.py\n@@ -81,6 +81,11 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):\n \n         TiffImagePlugin.TiffImageFile._open(self)\n \n+        # TiffImageFile._open() resets the frame count to that of the\n+        # embedded TIFF stream, but the frames of an MIC file are its streams\n+        self._n_frames = len(self.images)\n+        self.is_animated = self._n_frames > 1\n+\n         self.frame = frame\n \n     def tell(self) -> int:\n```\n\n## The reproducer\n\n<details>\n<summary>Reproducer — <code>repro</code>, 2,330 bytes (click to open)</summary>\n\nIts last line is `BUG` when the issue triggers and `OK` when it does not. Re-run on a clean build of that commit it printed `BUG`.\n\n```python\n#!/usr/bin/env python3\n# MicImageFile inherits TiffImageFile.n_frames, whose loop `self._seek(self.tell()+1)`\n# relies on tell() advancing; MicImageFile.tell() returns the OLE stream index (constant),\n# so a MIC file whose Image stream is a TIFF with >=3 chained IFDs makes im.n_frames spin forever.\nimport io, struct, subprocess, sys, tempfile, os\nfrom PIL import Image\n\ndef tiff(n):  # n-page TIFF written by Pillow itself\n    b = io.BytesIO(); fr = [Image.new(\"L\", (4, 4), i) for i in range(n)]\n    fr[0].save(b, \"TIFF\", save_all=True, append_images=fr[1:]); return b.getvalue()\n\ndef mic(stream):  # minimal OLE2 file: storage \"img0.ACI\" containing stream \"Image\"\n    END, FREE, FATSECT = 0xFFFFFFFE, 0xFFFFFFFF, 0xFFFFFFFD\n    data = stream.ljust(4096, b\"\\0\"); n = len(data) // 512\n    fat = [FATSECT, END] + [3 + k for k in range(n - 1)] + [END]; fat += [FREE] * (128 - len(fat))\n    def entry(name, kind, child, start, size):\n        raw = name.encode(\"utf-16-le\") + b\"\\0\\0\"\n        return raw.ljust(64, b\"\\0\") + struct.pack(\"<HBBiii\", len(raw), kind, 1, -1, -1, child) + b\"\\0\"*36 + struct.pack(\"<III\", start, size, 0)\n    d = entry(\"Root Entry\", 5, 1, END, 0) + entry(\"img0.ACI\", 1, 2, 0, 0) + entry(\"Image\", 2, -1, 2, len(data)) + b\"\\0\"*128\n    hdr = (b\"\\xd0\\xcf\\x11\\xe0\\xa1\\xb1\\x1a\\xe1\" + b\"\\0\"*16 + struct.pack(\"<HHHHH6x\", 0x3E, 3, 0xFFFE, 9, 6)\n           + struct.pack(\"<9I\", 0, 1, 1, 0, 4096, END, 0, END, 0) + struct.pack(\"<I\", 0) + b\"\\xff\"*432)\n    return hdr + struct.pack(\"<128I\", *fat) + d + data\n\nCHILD = \"import sys;from PIL import Image;im=Image.open(sys.argv[1]);print(im.format, 'n_frames =', im.n_frames)\"\ndef run(label, data, limit=20):\n    p = os.path.join(tempfile.mkdtemp(), \"x\"); open(p, \"wb\").write(data)\n    try:\n        r = subprocess.run([sys.executable, \"-c\", CHILD, p], capture_output=True, text=True, timeout=limit)\n        out = (r.stdout.strip() or r.stderr.strip().splitlines()[-1]); hung = False\n    except subprocess.TimeoutExpired:\n        out, hung = f\"STILL RUNNING after {limit}s (killed)\", True\n    print(f\"{label:32s} {len(data):6d} bytes -> {out}\"); return hung\n\nc1 = run(\"control: 3-page TIFF, direct\", tiff(3))\nc2 = run(\"control: MIC around 1-page TIFF\", mic(tiff(1)))\nbug = run(\"attack:  MIC around 3-page TIFF\", mic(tiff(3)))\nprint(\"BUG\" if bug and not c1 and not c2 else \"OK\")\n```\n</details>\n\n---","summary":"Pillow's MIC image plugin can be sent a crafted 5.6 KB file whose embedded TIFF has three or more chained directories, causing an infinite loop at 100% CPU when the application reads im.n_frames. An attacker can tie up a worker indefinitely with an uploaded image, and only an external watchdog stops it.","severity":"high","cvss":8.2,"epss":0.0029100000392645597,"exploited":false,"exploited_at":null,"products":[{"name":"Pillow","fixed":["13.0.0"],"affected":">= 7.2.0, < 13.0.0","ecosystem":"pypi"},{"name":"python pillow","fixed":["13.0.0"],"affected":">= 7.2.0, < 13.0.0","ecosystem":"vendor"}],"ecosystems":["pypi","vendor"],"tags":["cwe-835","image-upload","package","pillow","python","self-hosted"],"not_affected_if":["no-untrusted-image-input","olefile-not-installed","pillow-13.0.0-or-later"],"how_to_check":"Run pip show pillow and pip show olefile; the bug needs both installed and Pillow 7.2.0 up to but not including 13.0.0.\nSearch application code for n_frames, is_animated checks via getattr(im, \"n_frames\"), and save_all=True re-saves of untrusted uploads.","fix":"Upgrade Pillow to 13.0.0 or later (pip install -U \"pillow>=13.0.0\").\nIf you cannot upgrade yet, uninstall olefile or stop opening untrusted images with Pillow's MIC plugin; the source does not give a documented workaround beyond that.","status":"open","published_at":"2026-10-19T21:17:07+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T20:10:15+00:00","updated_at":"2026-10-23T22:28:57+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-10-23T20:10:15Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade Pillow to 13.0.0 or later (pip install -U \"pillow>=13.0.0\").\nIf you cannot upgrade yet, uninstall olefile or stop opening untrusted images with Pillow's MIC plugin; the source does not give a documented workaround beyond that.","summary":"Pillow's MIC image plugin can be sent a crafted 5.6 KB file whose embedded TIFF has three or more chained directories, causing an infinite loop at 100% CPU when the application reads im.n_frames. An attacker can tie up a worker indefinitely with an uploaded image, and only an external watchdog stops it.","category":"package","who_cares":["python","pillow","self-hosted","image-upload"],"confidence":0.85,"analyst_note":"","how_to_check":"Run pip show pillow and pip show olefile; the bug needs both installed and Pillow 7.2.0 up to but not including 13.0.0.\nSearch application code for n_frames, is_animated checks via getattr(im, \"n_frames\"), and save_all=True re-saves of untrusted uploads.\nunknown for any further exposure check.","needs_analyst":false,"not_affected_if":["olefile-not-installed","pillow-13.0.0-or-later","no-untrusted-image-input"],"severity_if_unknown":"unknown"},"cost_usd":0.000904,"input_tokens":6736,"output_tokens":461,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-112390':14A,22A '-2026':13A,21A '-835':27B '/image':468C '0':546C '0b33a87b0d15':339C '1':267C,370C,478C,479C,544C,589C,595C,628C '100':64B,120C,507C '1212':255C,599C '1219':531C,629C '122':550C '1271':525C '1622':519C '20':385C '2020':341C '22hg':17A '27':512C '3':100C,368C,580C '351':472C,572C '5.6':47B,111C '5632':460C '7.2.0':345C '8':549C '86':104C,283C 'a707c6b21c313880d0771e54965d142dd42358b6':108C 'accord':411C 'aci/image':405C 'advanc':432C 'affect':134C 'anim':167C,324C 'applic':68B,124C,143C 'april':340C 'around':587C 'ask':125C 'attach':355C 'attack':72B 'break':636C 'build':357C 'byte':216C,461C,473C,574C 'call':183C,290C,541C 'caus':59B 'chain':57B,229C,263C,477C 'chang':445C 'check':168C,363C 'code':361C 'commit':107C 'common':163C 'compos':207C 'compound':463C 'constant':286C 'contain':209C,249C,586C 'control':365C 'convert':322C 'core':505C 'cpu':65B,121C 'craft':46B,110C 'current':607C,612C,617C,631C 'cve':12A,20A 'cwe':26B 'def':602C 'detail':387C 'detect':489C 'direct':576C 'directori':58B,231C,262C,308C,318C 'embed':10A,51B,97C,259C,312C 'enough':482C 'evalu':497C 'everi':556C 'except':188C 'expect':373C 'extens':219C,495C 'extern':86B,197C 'file':49B,114C,202C,333C,359C,376C,464C,516C,522C,528C 'first':342C 'forev':118C,309C,458C 'format':487C 'frame':7A,94C,128C,243C,270C,295C,352C,371C,401C,418C,448C,534C,543C,547C,552C,579C,594C,604C,609C,614C 'ghsa':16A 'ghsa-22hg-r758-rrw9':15A 'give':577C,592C 'grow':192C 'hang':330C 'host':36B 'ident':558C 'ifd':101C,434C,457C,480C,590C 'im.n_frames':70B,152C,498C 'imag':29B,40B,82B,113C,130C,148C,166C,206C,230C 'image-upload':28B 'image.open':319C,391C,483C 'imagesequ':326C 'img0.aci':467C 'img0.aci/image':466C 'implement':424C,428C 'includ':161C 'indefinit':78B,159C 'index':288C,442C 'infinit':2A,61B,89C 'inherit':252C,422C 'insid':221C,446C 'instal':135C,395C 'instrument':536C 'int':606C 'introduc':337C 'iter':327C,557C 'kb':48B,112C 'keep':503C 'kill':383C 'len':620C 'line':518C,524C,530C 'load':320C,436C 'locat':102C 'loop':3A,62B,90C,117C,302C,451C,562C,597C 'magic':215C,493C 'make':115C,279C 'malici':375C 'mani':127C 'mean':244C 'memori':193C 'mere':568C 'mic':5A,39B,92C,208C,488C 'micimagefil':232C,348C,388C 'micimagefile.seek':289C 'micimagefile.tell':106C,281C,437C 'microsoft':205C 'millisecond':582C 'n':6A,93C,269C,294C,351C,417C,447C,533C,551C,578C,593C,603C,608C,613C 'need':220C,496C 'never':184C,444C,500C 'non':565C 'non-termin':564C 'none':297C,554C,616C,625C 'ole':404C,440C,492C 'ole2':462C 'olefil':140C,393C 'one':155C,245C,403C,504C 'open':145C,575C 'option':139C 'overrid':236C,407C,415C,634C 'packag':31B,141C 'pars':305C,454C 'pdf':179C 'pillow':1A,23A,25A,32B,37B,116C,211C 'plugin':41B 'pos':548C 'progress':280C,638C 'properti':353C,419C,601C 'python':24A,33B,509C,600C 'r758':18A 'rais':186C 're':171C,304C,453C 're-pars':303C,452C 're-sav':170C 'read':69B,151C 'recognis':212C 'redefin':399C 'regist':389C 'regress':335C 'releas':343C 'reli':429C 'remov':347C 'reproduc':356C 'reset':293C 'result':300C 'return':185C,284C,367C,438C,501C 'rrw9':19A 'run':379C,425C 'save':172C,174C 'second':307C,386C 'seek':237C,408C,527C,537C 'self':35B,605C 'self-host':34B 'self._frame_pos':621C 'self._n_frames':610C,623C 'self._seek':265C,619C,626C 'self.seek':630C 'self.tell':266C,618C,627C 'sent':44B 'set':272C 'setup':521C 'show':538C 'slow':569C 'src/pil/micimageplugin.py':103C,282C 'src/pil/tiffimageplugin.py':254C,517C,523C,529C,598C 'stack':510C 'state':559C 'still':251C,378C 'stop':88B,199C,275C 'stream':226C,246C,287C,313C,406C,441C,469C 'subclass':233C,396C 'succeed':486C 'taken':511C 'tell':239C,410C,431C,545C 'termin':566C 'thing':274C 'three':54B,228C,315C,476C 'thumbnail':321C 'tie':74B,157C 'tiff':11A,52B,98C,178C,225C,260C,423C,474C,573C,591C 'tiffimagefil':234C,397C 'tiffimagefile._open':291C 'tiffimagefile.n_frames':253C 'true':176C 'two':273C,364C 'untrust':147C 'upload':30B,81B 'walk':257C,277C 'watchdog':87B,198C 'webp':181C 'whenev':310C,392C 'whose':50B,465C 'worker':77B,156C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-19T21:17:07+00:00","kev":null,"cvss_vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-22hg-r758-rrw9","url":"https://github.com/advisories/GHSA-22hg-r758-rrw9","severity":"high","published_at":"2026-10-21T20:39:49+00:00","modified_at":"2026-10-21T20:39:50+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-112390","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112390","severity":null,"published_at":"2026-10-19T21:17:07+00:00","modified_at":"2026-10-23T17:01:18+00:00","first_seen_at":"2026-10-23T21:57:19+00:00","fetched_at":"2026-10-23T22:28:57+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}