From ec3c3bd599ac6a8e938d9a1e2f3ddfb662c2df20 Mon Sep 17 00:00:00 2001 From: Vulnerability Bot Date: Wed, 11 Nov 2026 13:51:03 +0100 Subject: [PATCH] vuln-osv update 2026-11-11T12:50:56Z --- data/PyPI/GHSA-xvh5-g6gv-559v.json | 2 +- data/PyPI/MAL-2026-19116.json | 1 + data/PyPI/PYSEC-2026-4363.json | 1 + data/crates.io/RUSTSEC-2026-0269.json | 2 +- data/crates.io/RUSTSEC-2026-0294.json | 2 +- data/crates.io/RUSTSEC-2026-0480.json | 1 + data/crates.io/RUSTSEC-2026-0481.json | 1 + data/crates.io/RUSTSEC-2026-0482.json | 1 + data/crates.io/RUSTSEC-2026-0483.json | 1 + data/crates.io/RUSTSEC-2026-0484.json | 1 + data/crates.io/RUSTSEC-2026-0485.json | 1 + data/crates.io/RUSTSEC-2026-0486.json | 1 + data/crates.io/RUSTSEC-2026-0487.json | 1 + data/crates.io/RUSTSEC-2026-0488.json | 1 + data/crates.io/RUSTSEC-2026-0489.json | 1 + data/crates.io/RUSTSEC-2026-0490.json | 1 + data/crates.io/RUSTSEC-2026-0491.json | 1 + data/npm/MAL-2026-18794.json | 2 +- data/npm/MAL-2026-19118.json | 1 + state/sync.json | 24 ++++++++++++------------ 20 files changed, 31 insertions(+), 16 deletions(-) create mode 100644 data/PyPI/MAL-2026-19116.json create mode 100644 data/PyPI/PYSEC-2026-4363.json create mode 100644 data/crates.io/RUSTSEC-2026-0480.json create mode 100644 data/crates.io/RUSTSEC-2026-0481.json create mode 100644 data/crates.io/RUSTSEC-2026-0482.json create mode 100644 data/crates.io/RUSTSEC-2026-0483.json create mode 100644 data/crates.io/RUSTSEC-2026-0484.json create mode 100644 data/crates.io/RUSTSEC-2026-0485.json create mode 100644 data/crates.io/RUSTSEC-2026-0486.json create mode 100644 data/crates.io/RUSTSEC-2026-0487.json create mode 100644 data/crates.io/RUSTSEC-2026-0488.json create mode 100644 data/crates.io/RUSTSEC-2026-0489.json create mode 100644 data/crates.io/RUSTSEC-2026-0490.json create mode 100644 data/crates.io/RUSTSEC-2026-0491.json create mode 100644 data/npm/MAL-2026-19118.json diff --git a/data/PyPI/GHSA-xvh5-g6gv-559v.json b/data/PyPI/GHSA-xvh5-g6gv-559v.json index e5d3075cab..8ee8d1541c 100644 --- a/data/PyPI/GHSA-xvh5-g6gv-559v.json +++ b/data/PyPI/GHSA-xvh5-g6gv-559v.json @@ -1 +1 @@ -{"schema_version":"1.9.2","id":"GHSA-xvh5-g6gv-559v","published":"2026-10-21T21:29:16Z","modified":"2026-11-10T19:46:39.635737093Z","aliases":["CVE-2026-112902"],"summary":" Pillow: Memory disclosure and segfault in libtiff encoder via crafted TIFF tags","details":"```\n============================================================================\nMemory disclosure and segfault in libtiff encoder via crafted TIFF tags\nLocation: src/encode.c:1014 in PyImaging_LibTiffEncoderNew()\nCommit: a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nA crafted TIFF file of a few hundred bytes makes Pillow copy an attacker-chosen amount of its own process memory into the TIFF it writes back out, or crash the process outright. The trigger is an ordinary `Image.open()` followed by a bare `im.save(fp, \"TIFF\")`. The only precondition is that the application re-saves images as TIFF. When the uploaded file is compressed, Pillow routes that save through libtiff automatically, with no options from the application. The attached reproducer does this with a 356-byte LZW TIFF: one crafted tag comes back holding 65536 bytes taken from `_imaging.so`'s read-only data, and a second crafted tag makes the worker die with SIGSEGV on every run.\n\nThe bug is in `PyImaging_LibTiffEncoderNew` (`src/encode.c`). It decides what arguments to pass to the variadic `TIFFSetField()` from the shape of the Python value (scalar, tuple or bytes). It assumes its `ImagingLibTiffMergeFieldInfo()` call defined how libtiff will read them. But `TIFFMergeFieldInfo()` never overrides libtiff's built-in field table, so libtiff reads the arguments according to its own definition of the tag:\n\n- For a counted array tag such as DNGPrivateData (50740), a count-1 integer in the source file is taken as the byte count. The data pointer is then read from an argument slot Pillow never wrote, and that many bytes are copied into the output.\n- For a fixed-size array tag such as ActiveArea (50829), the integer itself is used as the source address.\n\nWhere that unwritten slot points depends on the compiler. In the tested clang build it points into `_imaging.so`, and a larger count returned thousands of pointers into libpython, libc and libtiff, which defeats address-space randomisation for anyone chaining another bug. When the application preserves metadata with `tiffinfo=im.tag_v2`, a 64-bit entry turns ActiveArea into a 16-byte read from an address the attacker chooses. All observed memory errors are reads; no out-of-bounds write was found.\n\nOther branches of the same loop share the defect and are reachable from crafted TIFFs or from EXIF in JPEG/PNG/WebP re-saved as compressed TIFF. These are the `bytes`, RATIONAL-tuple and typed-tuple branches, and a BYTE-typed ColorMap. The file-reachable forwarding was introduced in `a3d45e9cefd0` (\"Added custom int and float TIFF tags\") and first released in 5.4.0.\n\n## Details\n\n`PyImaging_LibTiffEncoderNew` (`src/encode.c:777-1060`) chooses the argument\nlist it hands to the variadic `TIFFSetField()` from the *Python* value of each\ntag — scalar vs. tuple vs. `bytes`, and a type that is either autodetected or\ncopied from the source file's IFD — and assumes that its own\n`ImagingLibTiffMergeFieldInfo()` registration defines how libtiff will read the\n`va_list`. It does not: libtiff keeps its built-in definition for every tag it\nalready knows, and core tags are never registered at all. libtiff therefore\nconsumes the varargs as `(count, pointer)`, `(pointer)` or three pointers\naccording to *its* field table, whatever Pillow actually pushed. The encoder\nnever consults that table (`TIFFFindField` / `TIFFFieldPassCount` /\n`TIFFFieldWriteCount` / `TIFFFieldDataType`) before the call.\n\nThe most severe instance, and the one the attached reproducer exercises: when a\nTIFF is re-saved through the libtiff writer, every tag of the source IFD that\nPillow does not recognise is forwarded to libtiff with `TIFFSetField()`. A\ncount-1 integer entry is passed as a single scalar vararg. If libtiff's own\nfield table defines that tag number as an *array* field, libtiff consumes the\nvarargs as `(count, pointer)` or `(pointer)` instead, so the attacker's integer\nbecomes either the number of bytes to copy from a never-written vararg slot, or\nthe source address itself. The same mismatch is reachable through the typed\ntuple branches (tuple length consumed as a pointer for fixed-count built-in\ntags such as `CFARepeatPatternDim`, even when the source file is well-formed),\nthe `bytes`, `TIFF_RATIONAL`-tuple and core-tag branches of the same loop;\nthose are listed\nunder *Other branches with the same defect* below and need to be covered by the\nsame fix.\n\nA 181-byte TIFF (16×16 RGB, one LZW strip, plus one extra IFD entry\n`50740 / LONG / count 1 / value 4096` — DNGPrivateData) opened with\n`Image.open()` and saved with a bare `im.save(buf, \"TIFF\")` produces a 4296-byte\noutput whose tag 50740 holds 4096 bytes that occur nowhere in the input; scanning\n`/proc/self/mem` finds them in the read-only data segment of `_imaging.so`.\nChanging the extra entry to `50829 / LONG / 1 / 64` (ActiveArea) makes the same\ntwo calls segfault in `memcpy(dst, src=0x40, n=16)` under\n`ImagingLibTiffSetField` ← `PyImaging_LibTiffEncoderNew`. An ASan build of the\nextensions reports `READ of size 4096` in `memcpy` called from\n`src/encode.c:1014` for the first input.\n\n### Code path\n\n`_save` copies all tags of the opened image into the libtiff attribute list.\nFor a tag that is neither in `LIBTIFF_CORE` nor in `TiffTags.TAGS_V2`, no type\nis recorded, so the value goes through untyped:\n\n```python\n# src/PIL/TiffImagePlugin.py:1782\nsupplied_tags = {**legacy_ifd, **getattr(im, \"tag_v2\", {})}\n…\n# :1959-1982\nfor tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n if tag not in TiffTags.LIBTIFF_CORE:\n …\n elif isinstance(value, (int, float, str, bytes)) or …:\n type = TiffTags.lookup(tag).type # None for 50740, 50829, …\n if type:\n types[tag] = type\n if tag not in atts and tag not in blocklist:\n …\n atts[tag] = value # plain int 4096\n```\n\n`compression` defaults to the source image's `im.info[\"compression\"]`\n(`TiffImagePlugin.py:1731-1746`), so a compressed source selects the libtiff\nwriter without the application asking for it.\n\nIn the encoder the untyped `int` is autodetected as `TIFF_LONG`\n(`src/encode.c:805-815`), the tag is \"registered\" as a scalar custom field, and\nthen set with one 32-bit vararg:\n\n```c\n// src/encode.c:844 (is_var_length = 0 → writecount 1, passcount 0)\nif (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) {\n … continue;\n}\n…\n// src/encode.c:1013-1016\n} else if (type == TIFF_LONG || type == TIFF_IFD) {\n status = ImagingLibTiffSetField(\n &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)\n );\n```\n\nThe registration does not do what the caller assumes. `TIFFMergeFieldInfo()`\nonly adds definitions for tags libtiff does not already know and returns 0\n(success) either way, so for tags in libtiff's built-in table the built-in\ndefinition stays in force. Queried against the linked libtiff 4.5.0 after\nexactly Pillow's merge call:\n\n```\n50740 DNGPrivateData type=BYTE passcount=1 writecount=-1 (unchanged)\n50829 ActiveArea type=LONG passcount=0 writecount=4 (unchanged)\n65000 CustomField type=LONG passcount=0 writecount=1 (Pillow's definition, tag was unknown)\n```\n\nlibtiff's setter for `FIELD_CUSTOM` tags then interprets the `va_list`\naccording to *its* definition: for `passcount=1` it reads `va_arg(ap, int)` as\nthe element count — the attacker's 4096 — allocates `count * size` bytes and\n`memcpy`s that many bytes from `va_arg(ap, void *)`, a slot\n`PyImaging_LibTiffEncoderNew` never wrote. On x86-64 that slot is register\n`rcx`; in this build it holds a pointer into `_imaging.so`'s `.rodata`, so the\ncopy succeeds deterministically (a value of 2 000 000 also succeeds and yields a\n2 000 200-byte output). `TIFFWriteDirectory` writes the copied bytes into the\noutput file as the tag's value. For fixed-count array fields (`passcount=0`,\n`writecount=4` for ActiveArea) there is no count argument, so the attacker's\nscalar is consumed as the `void *` source and 16 bytes are read from address\n64 → SIGSEGV.\n\nTags in libtiff 4.5's built-in table that Pillow forwards untyped and that take\nthe `(count, pointer)` form include DNGPrivateData 50740, CFAPattern 33422,\nCFAPlaneColor 50710, LinearizationTable 50712, BlackLevel 50714, WhiteLevel\n50717, ColorMatrix1/2 50721/50722, AsShotNeutral 50728, OriginalRawFileName/Data\n50827/50828, AsShotICCProfile 50831, CurrentICCProfile 50833. Fixed-count ones\n(pointer only → crash) include ActiveArea 50829, DNGVersion/DNGBackwardVersion\n50706/50707, DefaultScale/DefaultCropOrigin/DefaultCropSize 50718–50720,\nAsShotWhiteXY 50729, CFARepeatPatternDim 33421, BlackLevelRepeatDim 50713,\nImageLayer 34732, RawDataUniqueID 50781.\n\nWhen the application saves with `tiffinfo=im.tag_v2` (the documented way to\npreserve metadata), the file-declared type is honoured, and a `LONG8` entry\nreaches the `(uint64_t)PyLong_AsLongLong(value)` branch at\n`src/encode.c:1049-1052`. For ActiveArea that is a 16-byte read from a full\n64-bit attacker-chosen address into the output: a source entry\n`50829 / LONG8 / 1 /
` produced an output\nActiveArea of `(b'SECR', b'ET-T', b'OKEN', b'-012')` in testing.\n\nThe comment above `blocklist +=` at `TiffImagePlugin.py:1938-1944` already notes\nthat array tags with the wrong length \"will result in segfaults\"; the blocklist\ncovers only a handful of tags Pillow itself knows, not the ones libtiff knows and\nPillow does not. Forwarding of these tags was introduced in `a3d45e9cefd0`\n(\"Added custom int and float TIFF tags\", 2018-10-25), first released in 5.4.0;\nreleases 2.0.0–3.0.0 forwarded them as scalars too, until `9fa6634f4`\n(\"Partial fix for #1597\") limited `_save` to `LIBTIFF_CORE`.\n\n### Other branches with the same defect\n\nThe scalar-integer branch above is one of several places in the same loop\nwhere the call shape is derived from the Python value rather than from\nlibtiff's definition. All of the following were confirmed on this commit (stock\nbuild crashes and/or ASan reports, stack always `memcpy ← libtiff ←\nImagingLibTiffSetField src/libImaging/TiffDecode.c:934 ←\nPyImaging_LibTiffEncoderNew`), and all are reachable from a sub-1 KB crafted\nfile followed by an ordinary `save()`:\n\n* **Typed tuple → `(len, av)` for a fixed-count built-in field**\n (`src/encode.c:904-994`, the SHORT/LONG/SBYTE/SSHORT/SLONG/FLOAT/DOUBLE\n tuple branches all call `ImagingLibTiffSetField(..., len, av)`). For libtiff\n built-in fields with `passcount=0` and a fixed `writecount` —\n `CFARepeatPatternDim` 33421 (SHORT×2), `BlackLevelRepeatDim` 50713\n (SHORT×2), `ImageLayer` 34732 (LONG×2), `ActiveArea` 50829 (LONG×4),\n `MatrixWorldToScreen/Camera` 33305/33306 (FLOAT×16) — libtiff takes the\n *first* vararg as the source pointer, so the tuple length becomes the\n `memcpy` source address. A perfectly well-formed 206-byte 8×8 PackBits TIFF\n carrying `33421 / SHORT / 2 / (2, 2)` re-saved with a bare\n `im.save(out, \"TIFF\")` dies with SIGSEGV in `memcpy` (gdb: `rsi = 0x2`, the\n tuple length) on every run; so does\n `Image.new(\"L\", (8, 8)).save(buf, \"TIFF\", compression=\"packbits\",\n tiffinfo={33421: (2, 2)})`, and a JPEG whose EXIF carries the tag saved with\n `exif=im.getexif()` into a compressed TIFF. `bytes` values for built-in\n fixed-count BYTE tags (e.g. `DNGVersion` 50706, BYTE×4) crash the same way\n through the `(PyBytes_Size, PyBytes_AsString)` call at `src/encode.c:860-865`.\n The mirror image for `passcount=1` fields: `Photoshop` 34377 declared\n SHORT, count 1, value 60000 takes the scalar branch at `:1009-1012`, libtiff\n reads 60000 as the count and an unpassed vararg as the pointer, and the\n re-save *succeeds* with a 60162-byte output whose tag 34377 holds 60000 bytes\n of process memory (here C prototype strings from `_imaging.so`'s `.rodata`).\n* **`bytes` value → single `char *`** (`src/encode.c:805-815` autodetects\n `bytes` as `TIFF_ASCII`; `:1033-1042` passes `PyBytes_AsString(value)` alone).\n `ImageFileDirectory_v2._setitem` stores a BYTE-typed entry as bare `bytes`\n and an ASCII-typed one as `str` (encoded to `bytes` by `_save`), so the\n file's declared type selects this branch.\n - `ColorMap` (320) declared BYTE with 4 bytes of data in a non-palette\n PackBits TIFF: 320 is in `core_tags[]`, no type is consulted, the value\n skips the 768-tuple `TIFFTAG_COLORMAP` branch (`:871-895`) and libtiff reads\n three `uint16_t *` and copies `1 << BitsPerSample` shorts from each — a\n 512-byte over-read of the 5-byte Python bytes buffer, then dereference of\n two stale argument registers → SIGSEGV on every run of\n `Image.open(f); im.save(out, \"TIFF\")`. `COLORMAP` is not in `_save`'s\n `blocklist`.\n - `Photoshop` (34377) declared ASCII: `_save` copies value *and* type\n (`TiffImagePlugin.py:1802-1818`; only `IPTC_NAA_CHUNK` has a BYTE/UNDEFINED\n type guard there, from commit 2245df0a — `PHOTOSHOP_CHUNK` and `XMP` do\n not). libtiff's definition is BYTE, `passcount=1`, so the low 32 bits of\n the `char *` become the byte count for a `memcpy` from a stale register\n (ASan: `READ of size 3518384`; stock: SIGSEGV in about half of runs,\n otherwise `RuntimeError`). `ICCProfile` (34675) declared ASCII behaves the\n same way via `im.info[\"icc_profile\"]` → `ifd[ICCPROFILE]`\n (`TiffImagePlugin.py:1822-1824`).\n* **`TIFF_RATIONAL` tuple → array with no count** (`src/encode.c:995-1006`;\n compare the `TIFF_FLOAT` branch at `:969-981`, which passes `len, av`).\n - For a private tag (e.g. 50000) Pillow itself registers the field with\n `passcount=1` (`TiffDecode.c:904-908`), so libtiff takes the low 32 bits of\n `av` as the element count and the next stale vararg as the source:\n `heap-buffer-overflow READ of size 119872 … right of 8-byte region` under\n ASan (`119872 == 4 * low32(av)`), SIGSEGV in ~4 of 5 stock runs. Reached by\n a JPEG whose EXIF carries one private RATIONAL×2 tag saved with\n `im.save(out, \"TIFF\", compression=\"tiff_lzw\", exif=im.info[\"exif\"])`, or a\n TIFF re-saved with `tiffinfo=im.tag_v2` (`_setitem` auto-types a tuple of\n `IFDRational` as RATIONAL, `TiffImagePlugin.py:689-701`; `types[tag] =\n ifd.tagtype[tag]` at `:1968`).\n - For libtiff's built-in fixed-count float tags `WhitePoint` (318, 2),\n `PrimaryChromaticities` (319, 6) and `YCbCrCoefficients` (529, 3), which\n `_save` forwards from any source TIFF and types RATIONAL via\n `TiffTags.lookup`, the pointer-only convention happens to match, but\n libtiff copies its *fixed* count from an array that is only as long as the\n file declared: a PackBits TIFF with `PrimaryChromaticities` count 1 and a\n bare `im.save(out, \"TIFF\")` gives `READ of size 24 … right of 4-byte\n region`, and the over-read floats are written to tag 319 of the output.\n `REFERENCEBLACKWHITE`/`TRANSFERFUNCTION` are blocklisted at\n `TiffImagePlugin.py:1941-1952` for exactly this reason; 318/319/529 are not.\n* **Core tag reached as a scalar via a stale file-supplied type.** With the\n re-save idiom `im.save(out, \"TIFF\", tiffinfo=im.tag_v2)`, `_save` copies\n `ifd.tagtype[key] = info.tagtype[key]` for every key\n (`TiffImagePlugin.py:1768-1776`) and later assigns its own values to some of\n the same tags without resetting the type (`ifd[COLORMAP] = colormap` at\n `:1870-1877`; likewise BitsPerSample, SamplesPerPixel, ExtraSamples,\n ICCProfile, …). For a 418-byte PackBits *palette* TIFF whose ColorMap entry\n is declared BYTE, `_setitem` (`:751-770`) truncates `_save`'s 768-entry\n palette to a single `int` (with only a \"too many entries\" warning), the\n encoder receives `(320, 0)`, autodetects `TIFF_LONG`, and\n `src/encode.c:1013-1016` calls `TIFFSetField(TIFFTAG_COLORMAP, (UINT32)0)` —\n libtiff reads three `uint16_t *` and faults at an address ≤ 65280 on every\n run. The stale-tagtype copy is a Python-side hygiene bug in its own right,\n but its memory-safety consequence exists only because the encoder lets a\n non-tuple value reach `TIFFSetField(COLORMAP)`.\n\n## Impact\n\nAny service that accepts a TIFF and writes it back out as TIFF through libtiff —\nwhich happens with a plain `im.save(fp, \"TIFF\")` whenever the uploaded file is\ncompressed, or whenever a `compression=` other than raw is requested — returns\nto the uploader, inside one metadata tag of the produced file, an\nattacker-chosen number of bytes read from a stale pointer in the worker process,\ndeterministically and without retries. Which memory that is depends on the\ncompiler's register allocation for `PyImaging_LibTiffEncoderNew`: in this clang\nbuild the pointer is `_imaging.so + 0x1588`, so a request for 262 144 bytes\n(which succeeded every time in testing, as did 2 000 000) covers the\nextension's `.rodata`, `.data.rel.ro`/GOT and `.data` and returned 2 800+\nresolved pointers into libpython, libc, libtiff, libjpeg and others — a complete\nASLR bypass for chaining with any other memory bug; a gcc build of the same\nsource left a different, unrelated address in that slot. The fixed-count\nvariant kills the worker process with SIGSEGV from a 181-byte upload on every\nrequest. With `tiffinfo=im.tag_v2` the source\naddress is fully attacker-chosen (16-byte arbitrary read into the output for\nActiveArea, or a crash). No non-default Pillow setting is involved; the only\nprecondition is TIFF-to-TIFF re-encoding.\n\nThe other branches add further deterministic process-kill inputs (BYTE-typed\nColorMap in any RGB or palette TIFF; `thumbnail()` keeps `tag_v2`, so\nopen→thumbnail→save-as-TIFF is affected too) and extend the reach to\nJPEG/PNG/WebP uploads whose EXIF is preserved with `exif=` when converting to\nlibtiff-compressed TIFF. All the memory errors are reads; libtiff sizes its\ndestination buffers with the same count it copies, so no out-of-bounds write\nwas found.\n\n## Reproducing\n\n`repro` is one Python script, run against the stock build with the installed `PIL`. It needs no compiler and no extra files.\n\n`upload(tag, value)` has Pillow write a 16×16 red RGB image as an LZW-compressed TIFF, then appends a new IFD holding the same entries plus one extra ` / LONG / count 1 / value` (kept in tag order) and points the header at it — a 356-byte input on this tree. The source is LZW-compressed on purpose. A bare `im.save(out, \"TIFF\")` keeps the source's compression, so the save goes through the libtiff writer even though the application passes no arguments. `resave()` is the whole \"application\": `Image.open(BytesIO(src)); im.save(out, \"TIFF\")`.\n\nThe script runs three cases:\n\n1. **Control, tag 65000 = 4096.** libtiff does not know this tag, so the file is re-saved correctly and the output tag reads back as the integer `4096`. This is what should happen to every tag.\n2. **Leak, DNGPrivateData 50740 = 65536.** libtiff's built-in definition of this tag is a counted BYTE array. It reads Pillow's single scalar as the *count* and takes the data pointer from a vararg slot Pillow never wrote. The output tag comes back as `bytes` of length 65536 that do not occur in the input. The script prints the first 32 of them and the file offset at which they occur in `_imaging.so` on disk. On this build they always do (offset `0x57588`): the stale pointer aims into the extension's read-only data.\n3. **Wild read, ActiveArea 50829 = 64.** libtiff defines this tag as a fixed LONG[4] array with no count, so it uses the scalar 64 as the *source address*. The script re-runs itself as a child process (`repro child`, core dumps disabled) to do this re-save, and the child exits with `-11`, meaning SIGSEGV.\n\nExpected output on the affected tree:\n\n```\ncontrol tag 65000=4096 -> 4096\nleak tag 50740=65536 from a 356-byte upload -> 65536 bytes not present in input, head a661fcffc661fcff3360fcff7b61fcffd361fcff085ffcffe061fcff3360fcff\n leaked bytes found in _imaging.so at file offset 0x57588\ncrash tag 50829=64 -> child exit -11 (SIGSEGV)\nBUG\n```\n\nThe script prints `BUG` if case 2 leaks or case 3 crashes. Both are deterministic on this build. A fixed Pillow prints `OK`: the tag does not come back as 65536 bytes of foreign data (it reads back as `None`, or `save()` raises `ValueError`/`RuntimeError`, which the script catches), and the child exits with something other than SIGSEGV (0, or 1 if `save()` raised).\n\nThe other branches can be checked by hand on the stock build in the same way\n(each as `Image.open(f); im.save(BytesIO(), \"TIFF\")` in a child process):\na PackBits RGB TIFF with an extra IFD entry `320 / BYTE / 4 / \"ABCD\"` →\nSIGSEGV every run; a PackBits palette TIFF whose ColorMap is `320 / BYTE / 2`\nsaved with `tiffinfo=im.tag_v2` → SIGSEGV every run; a Pillow-written JPEG\nwhose EXIF holds tag 50000 = RATIONAL×2 saved with\n`compression=\"tiff_lzw\", exif=im.info[\"exif\"]` → SIGSEGV in most runs,\n`RuntimeError` (\"Failed to allocate memory for custom tag binary object\n(-N elements of 4 bytes each)\") in the rest.\n\n## Suggested fix\n\nThe encoder must not guess the calling convention; it has to ask libtiff what\nthe tag's effective definition is *after* `ImagingLibTiffMergeFieldInfo` and\nmarshal accordingly, or refuse. The attached patch does this for non-core tags:\n\n* a new `ImagingLibTiffFindField()` in `src/libImaging/TiffDecode.c` (declared\n in `TiffDecode.h`, next to the other libtiff wrappers) returns libtiff's\n `TIFFFindField` type, `passcount` and `writecount` for a tag;\n* in `PyImaging_LibTiffEncoderNew` (`src/encode.c`), for a non-core tag that\n libtiff already knows, Pillow adopts libtiff's type when it has none of its\n own, and after `ImagingLibTiffMergeFieldInfo` a new `tiff_field_matches()`\n checks the value against libtiff's definition: a scalar only for a\n single-value field whose C argument type (int / `uint64_t` / double /\n `char *`) matches, an array only with libtiff's element size and, for\n fixed-count fields, exactly `writecount` elements. Anything else raises\n `ValueError(\"Tag value does not match libtiff field definition\")` instead of\n reaching `TIFFSetField` — this covers the reproducer's DNGPrivateData and\n ActiveArea inputs, ASCII-typed Photoshop, short PrimaryChromaticities and the\n `LONG8` `tiffinfo=` variant;\n* every tuple/`bytes` branch now goes through `set_tiff_array_field()`, which\n passes `len` only when the field has `passcount` set; this also fixes the\n `TIFF_RATIONAL` branch, which never passed a count (and extends it to\n `TIFF_SRATIONAL`);\n* `TIFF_UNDEFINED` fields are registered as variable-length, matching how their\n `bytes` values are passed;\n* `Tests/test_file_libtiff.py` gains a regression test (scalar DNGPrivateData,\n scalar and 2-tuple ActiveArea rejected; well-formed values round-trip).\n\nCore tags and tags libtiff does not know keep their current behaviour. With the\npatch applied to a clean checkout and rebuilt, the reproducer no longer\ntriggers and the test suite passes. It is not a complete fix, however: the\nwhole check sits inside `if (!is_core_tag)`, so the BYTE-typed ColorMap variant\nstays open. The PackBits RGB TIFF carrying `320 / BYTE / 4`, re-saved with a\nbare `save()`, no longer segfaults at this point but writes 1536 bytes —\nover-read from the 5-byte `bytes` object and two stale varargs, including 22\nlive heap pointers — into the output ColorMap, which is still a disclosure.\nBefore merging, it needs a guard that accepts `TIFFTAG_COLORMAP` only through\nthe existing 768-tuple branch, never through the scalar or `bytes` branches,\nand ideally the same shape check for the other core tags libtiff special-cases\nin `_TIFFVSetField` (YCbCrSubsampling 530, TransferFunction 301,\nReferenceBlackWhite 532).\n\nNarrower Python-side mitigations in `_save` close individual entry points but\nnot the class, and are worth adding alongside the C-side check: drop tags\nwith `TiffTags.lookup(tag).type is None` from `supplied_tags` (closes the `Image.open()→save()` path shown here but not\n`tiffinfo=` callers); remove `COLORMAP` from `supplied_tags` next to\n`EXIFIFD`/`SAMPLEFORMAT` and extend the `IPTC_NAA_CHUNK` BYTE/UNDEFINED type\nguard to `PHOTOSHOP_CHUNK` and `XMP`; add 318/319/529 to the blocklist next to\n`REFERENCEBLACKWHITE`; and have `_save` `ifd.tagtype.pop(tag, None)` before\nassigning its own ColorMap/BitsPerSample/ICCProfile/… values so a\n`tiffinfo=`-imported type cannot reshape them. The C-side check is the fix to\nrely on.\n\n## The patch\n\nPass TIFF tag values to libtiff according to its own field definition, and raise ValueError when they do not fit\n\nApplies to `src/encode.c` 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/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py\nindex 3d804ea4a..adc4633e4 100644\n--- a/Tests/test_file_libtiff.py\n+++ b/Tests/test_file_libtiff.py\n@@ -395,6 +395,43 @@ class TestFileLibTiff(LibTiffTestCase):\n with pytest.raises(ValueError, match=\"Incorrect tag value type\"):\n im.save(out, tiffinfo=ifd)\n \n+ @pytest.mark.parametrize(\n+ \"tag, value\",\n+ (\n+ # DNGPrivateData is a BYTE array with a count to libtiff\n+ (50740, 4096),\n+ # ActiveArea is a LONG array of four values to libtiff\n+ (50829, 64),\n+ (50829, (0, 0)),\n+ ),\n+ )\n+ def test_libtiff_field_definition(\n+ self, tag: int, value: Any, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n+ ) -> None:\n+ monkeypatch.setattr(TiffImagePlugin, \"WRITE_LIBTIFF\", True)\n+\n+ ifd = TiffImagePlugin.ImageFileDirectory_v2()\n+ ifd[tag] = value\n+ ifd.tagtype[tag] = TiffTags.LONG\n+\n+ out = tmp_path / \"temp.tif\"\n+ im = Image.new(\"L\", (1, 1))\n+ with pytest.raises(ValueError, match=\"libtiff field definition\"):\n+ im.save(out, tiffinfo=ifd)\n+\n+ # Values that match the libtiff definition are written\n+ ifd = TiffImagePlugin.ImageFileDirectory_v2()\n+ ifd[50740] = b\"private\"\n+ ifd.tagtype[50740] = TiffTags.BYTE\n+ ifd[50829] = (0, 0, 1, 1)\n+ ifd.tagtype[50829] = TiffTags.LONG\n+ im.save(out, tiffinfo=ifd)\n+\n+ with Image.open(out) as reloaded:\n+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)\n+ assert reloaded.tag_v2[50740] == b\"private\"\n+ assert reloaded.tag_v2[50829] == (0, 0, 1, 1)\n+\n def test_inknames_tag(\n self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n ) -> None:\ndiff --git a/src/encode.c b/src/encode.c\nindex 9e19e9002..cbbd09f0d 100644\n--- a/src/encode.c\n+++ b/src/encode.c\n@@ -675,6 +675,106 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) {\n \n #include \n \n+/* C type that TIFFSetField reads from its arguments for a single value of\n+ the given type: int, uint64_t, double or char * */\n+static int\n+tiff_scalar_arg_type(TIFFDataType type) {\n+ switch (type) {\n+ case TIFF_BYTE:\n+ case TIFF_SBYTE:\n+ case TIFF_UNDEFINED:\n+ case TIFF_SHORT:\n+ case TIFF_SSHORT:\n+ case TIFF_LONG:\n+ case TIFF_SLONG:\n+ case TIFF_IFD:\n+ return 1;\n+ case TIFF_LONG8:\n+ case TIFF_SLONG8:\n+ case TIFF_IFD8:\n+ return 2;\n+ case TIFF_FLOAT:\n+ case TIFF_DOUBLE:\n+ case TIFF_RATIONAL:\n+ case TIFF_SRATIONAL:\n+ return 3;\n+ case TIFF_ASCII:\n+ return 4;\n+ default:\n+ return 0;\n+ }\n+}\n+\n+/* Size of the values that TIFFSetField copies from an array of the given\n+ type. libtiff stores custom RATIONAL arrays as floats. */\n+static int\n+tiff_array_item_size(TIFFDataType type) {\n+ switch (type) {\n+ case TIFF_BYTE:\n+ case TIFF_SBYTE:\n+ case TIFF_UNDEFINED:\n+ case TIFF_ASCII:\n+ return 1;\n+ case TIFF_SHORT:\n+ case TIFF_SSHORT:\n+ return 2;\n+ case TIFF_LONG:\n+ case TIFF_SLONG:\n+ case TIFF_IFD:\n+ case TIFF_FLOAT:\n+ case TIFF_RATIONAL:\n+ case TIFF_SRATIONAL:\n+ return 4;\n+ case TIFF_DOUBLE:\n+ case TIFF_LONG8:\n+ case TIFF_SLONG8:\n+ case TIFF_IFD8:\n+ return 8;\n+ default:\n+ return 0;\n+ }\n+}\n+\n+/* Check that a value which will be passed to TIFFSetField as a single value\n+ of the given type, or as an array of len values, matches the definition\n+ of the field that libtiff will use. */\n+static int\n+tiff_field_matches(\n+ TIFFDataType field_type,\n+ int passcount,\n+ int writecount,\n+ TIFFDataType type,\n+ int is_var_length,\n+ Py_ssize_t len\n+) {\n+ if (field_type == TIFF_ASCII && !passcount) {\n+ // TIFFSetField reads a null-terminated string\n+ return !is_var_length && type == TIFF_ASCII;\n+ }\n+ if (!is_var_length) {\n+ // TIFFSetField reads a single value\n+ return !passcount && writecount == 1 && tiff_scalar_arg_type(type) &&\n+ tiff_scalar_arg_type(type) == tiff_scalar_arg_type(field_type);\n+ }\n+ // TIFFSetField reads a pointer to the values, preceded by the number of\n+ // values if passcount is set. Otherwise, the number of values is fixed.\n+ if (!tiff_array_item_size(type) ||\n+ tiff_array_item_size(type) != tiff_array_item_size(field_type)) {\n+ return 0;\n+ }\n+ return passcount || (writecount > 1 && len == writecount);\n+}\n+\n+static int\n+set_tiff_array_field(\n+ ImagingCodecState state, int key, int passcount, Py_ssize_t len, void *av\n+) {\n+ if (passcount) {\n+ return ImagingLibTiffSetField(state, (ttag_t)key, len, av);\n+ }\n+ return ImagingLibTiffSetField(state, (ttag_t)key, av);\n+}\n+\n PyObject *\n PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n ImagingEncoderObject *encoder;\n@@ -689,7 +789,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n PyObject *key, *value;\n Py_ssize_t pos = 0;\n int key_int, status, is_core_tag, is_var_length, num_core_tags, i;\n- TIFFDataType type = TIFF_NOTYPE;\n+ int is_known_tag, passcount, writecount;\n+ TIFFDataType type = TIFF_NOTYPE, field_type;\n // This list also exists in TiffTags.py\n const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277,\n 278, 280, 281, 282, 283, 284, 286, 287, 296, 297,\n@@ -777,6 +878,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n status = 0;\n is_core_tag = 0;\n is_var_length = 0;\n+ is_known_tag = 0;\n+ passcount = 1;\n type = TIFF_NOTYPE;\n \n for (i = 0; i < num_core_tags; i++) {\n@@ -800,6 +903,15 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n }\n Py_DECREF(tag_type);\n }\n+\n+ // libtiff uses its own definition for tags that it already knows,\n+ // rather than the field registered below.\n+ is_known_tag = ImagingLibTiffFindField(\n+ &encoder->state, key_int, &field_type, &passcount, &writecount\n+ );\n+ if (is_known_tag && type == TIFF_NOTYPE) {\n+ type = field_type;\n+ }\n }\n \n if (type == TIFF_NOTYPE) {\n@@ -838,7 +950,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n \n if (!is_core_tag) {\n // Register field for non core tags.\n- if (type == TIFF_BYTE) {\n+ if (type == TIFF_BYTE || type == TIFF_UNDEFINED) {\n is_var_length = 1;\n }\n if (ImagingLibTiffMergeFieldInfo(\n@@ -847,6 +959,28 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n Py_DECREF(item);\n continue;\n }\n+\n+ if (is_known_tag) {\n+ // Check that the value will be passed to TIFFSetField in the\n+ // form that libtiff's definition of the tag expects.\n+ Py_ssize_t len = 0;\n+ if (PyTuple_Check(value)) {\n+ len = PyTuple_Size(value);\n+ } else if (PyBytes_Check(value)) {\n+ len = PyBytes_Size(value);\n+ }\n+ if (!tiff_field_matches(\n+ field_type, passcount, writecount, type, is_var_length, len\n+ )) {\n+ Py_DECREF(encoder);\n+ Py_DECREF(item);\n+ PyErr_SetString(\n+ PyExc_ValueError,\n+ \"Tag value does not match libtiff field definition\"\n+ );\n+ return NULL;\n+ }\n+ }\n }\n \n if (type == TIFF_BYTE || type == TIFF_UNDEFINED ||\n@@ -857,9 +991,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n PyErr_SetString(PyExc_ValueError, \"Incorrect tag value type\");\n return NULL;\n }\n- status = ImagingLibTiffSetField(\n+ status = set_tiff_array_field(\n &encoder->state,\n- (ttag_t)key_int,\n+ key_int,\n+ passcount,\n PyBytes_Size(value),\n PyBytes_AsString(value)\n );\n@@ -909,8 +1044,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -922,8 +1057,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -935,8 +1070,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -948,8 +1083,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -961,8 +1096,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -974,8 +1109,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -987,12 +1122,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n- } else if (type == TIFF_RATIONAL) {\n+ } else if (type == TIFF_RATIONAL || type == TIFF_SRATIONAL) {\n FLOAT32 *av;\n /* malloc check ok, calloc checks for overflow */\n av = calloc(len, sizeof(FLOAT32));\n@@ -1000,8 +1135,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status =\n- ImagingLibTiffSetField(&encoder->state, (ttag_t)key_int, av);\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n+ );\n free(av);\n }\n }\ndiff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c\nindex 8a57c523d..e14dd50bb 100644\n--- a/src/libImaging/TiffDecode.c\n+++ b/src/libImaging/TiffDecode.c\n@@ -923,6 +923,29 @@ ImagingLibTiffMergeFieldInfo(\n return TIFFMergeFieldInfo(clientstate->tiff, info, n);\n }\n \n+int\n+ImagingLibTiffFindField(\n+ ImagingCodecState state,\n+ int key,\n+ TIFFDataType *field_type,\n+ int *passcount,\n+ int *writecount\n+) {\n+ // Look up libtiff's own definition of a tag. TIFFMergeFieldInfo does not\n+ // replace the definition of a tag that libtiff already knows, so this is\n+ // what TIFFSetField will use for such tags.\n+ TIFFSTATE *clientstate = (TIFFSTATE *)state->context;\n+ const TIFFField *fip = TIFFFindField(clientstate->tiff, (ttag_t)key, TIFF_ANY);\n+\n+ if (!fip) {\n+ return 0;\n+ }\n+ *field_type = TIFFFieldDataType(fip);\n+ *passcount = TIFFFieldPassCount(fip);\n+ *writecount = TIFFFieldWriteCount(fip);\n+ return 1;\n+}\n+\n int\n ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {\n // after tif_dir.c->TIFFSetField.\ndiff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h\nindex c31d0b9d8..ee1e4a5f9 100644\n--- a/src/libImaging/TiffDecode.h\n+++ b/src/libImaging/TiffDecode.h\n@@ -41,6 +41,14 @@ ImagingLibTiffMergeFieldInfo(\n ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length\n );\n extern int\n+ImagingLibTiffFindField(\n+ ImagingCodecState state,\n+ int key,\n+ TIFFDataType *field_type,\n+ int *passcount,\n+ int *writecount\n+);\n+extern int\n ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...);\n \n /*\n```\n\n## The reproducer\n\n
\nReproducer — repro, 2,545 bytes (click to open)\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# Pillow libtiff writer: a count-1 integer tag copied from the source TIFF is passed to\n# TIFFSetField() as one scalar vararg; for tags libtiff itself defines as arrays it is\n# consumed as a byte COUNT (DNGPrivateData 50740 -> N bytes of process memory written\n# into the output) or as a POINTER (ActiveArea 50829 -> SIGSEGV). Application side is\n# the ordinary \"open upload, save as TIFF\" (compression is inherited -> libtiff writer).\nimport io, struct, subprocess, sys, resource\nfrom PIL import Image\n\ndef upload(tag, value):\n \"\"\"Pillow-written 16x16 LZW TIFF, plus one extra IFD entry LONG[1] = value.\"\"\"\n b = io.BytesIO()\n Image.new(\"RGB\", (16, 16), \"red\").save(b, \"TIFF\", compression=\"tiff_lzw\")\n data = bytearray(b.getvalue()); assert data[:4] == b\"II*\\0\"\n (off,) = struct.unpack_from(\" 1 and sys.argv[1] == \"child\":\n resource.setrlimit(resource.RLIMIT_CORE, (0, 0))\n resave(upload(50829, 64)); sys.exit(0)\n\nprint(\"PIL core:\", Image.core.__file__)\nctl = Image.open(io.BytesIO(resave(upload(65000, 4096)))).tag_v2.get(65000)\nprint(\"control tag 65000=4096 ->\", repr(ctl))\n\nN = 65536\nsrc = upload(50740, N)\ntry:\n v = Image.open(io.BytesIO(resave(src))).tag_v2.get(50740)\nexcept (ValueError, RuntimeError) as e: # a fixed Pillow may reject the tag\n v = e\nleak = isinstance(v, bytes) and len(v) == N and v[:64] not in src\nprint(f\"leak tag 50740={N} from a {len(src)}-byte upload ->\",\n f\"{len(v)} bytes not present in input, head {v[:32].hex()}\" if leak else repr(v)[:80])\nif leak:\n so = open(Image.core.__file__, \"rb\").read()\n print(\" leaked bytes found in _imaging.so at file offset\", hex(so.find(v[:256])))\n\nrc = subprocess.call([sys.executable, __file__, \"child\"])\ncrash = rc in (-11, 139)\nprint(\"crash tag 50829=64 -> child exit\", rc, \"(SIGSEGV)\" if crash else \"\")\nprint(\"BUG\" if leak or crash else \"OK\")\n```\n
\n\n---","affected":[{"package":{"name":"pillow","ecosystem":"PyPI","purl":"pkg:pypi/pillow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.4.0"},{"fixed":"13.0.0"}]}],"versions":["10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","5.4.0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"],"database_specific":{"last_known_affected_version_range":"<= 12.3.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-xvh5-g6gv-559v/GHSA-xvh5-g6gv-559v.json"}}],"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-xvh5-g6gv-559v"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112902"},{"type":"WEB","url":"https://github.com/github/advisory-database/pull/10903"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/pull/10227"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/570df030c0d30c2bf5336b2ab59f3902d8ab0b6d"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/af56ace297b0991d02988fcc2ea933e4824b4fb3"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"}],"database_specific":{"cwe_ids":["CWE-686"],"github_reviewed":true,"github_reviewed_at":"2026-10-21T21:29:16Z","nvd_published_at":"2026-11-05T21:18:36Z","severity":"HIGH"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N"}]} \ No newline at end of file +{"schema_version":"1.9.2","id":"GHSA-xvh5-g6gv-559v","published":"2026-10-21T21:29:16Z","modified":"2026-11-11T10:20:03.066593346Z","aliases":["CVE-2026-112902","PYSEC-2026-4363"],"summary":" Pillow: Memory disclosure and segfault in libtiff encoder via crafted TIFF tags","details":"```\n============================================================================\nMemory disclosure and segfault in libtiff encoder via crafted TIFF tags\nLocation: src/encode.c:1014 in PyImaging_LibTiffEncoderNew()\nCommit: a707c6b21c313880d0771e54965d142dd42358b6\n============================================================================\n```\n\nA crafted TIFF file of a few hundred bytes makes Pillow copy an attacker-chosen amount of its own process memory into the TIFF it writes back out, or crash the process outright. The trigger is an ordinary `Image.open()` followed by a bare `im.save(fp, \"TIFF\")`. The only precondition is that the application re-saves images as TIFF. When the uploaded file is compressed, Pillow routes that save through libtiff automatically, with no options from the application. The attached reproducer does this with a 356-byte LZW TIFF: one crafted tag comes back holding 65536 bytes taken from `_imaging.so`'s read-only data, and a second crafted tag makes the worker die with SIGSEGV on every run.\n\nThe bug is in `PyImaging_LibTiffEncoderNew` (`src/encode.c`). It decides what arguments to pass to the variadic `TIFFSetField()` from the shape of the Python value (scalar, tuple or bytes). It assumes its `ImagingLibTiffMergeFieldInfo()` call defined how libtiff will read them. But `TIFFMergeFieldInfo()` never overrides libtiff's built-in field table, so libtiff reads the arguments according to its own definition of the tag:\n\n- For a counted array tag such as DNGPrivateData (50740), a count-1 integer in the source file is taken as the byte count. The data pointer is then read from an argument slot Pillow never wrote, and that many bytes are copied into the output.\n- For a fixed-size array tag such as ActiveArea (50829), the integer itself is used as the source address.\n\nWhere that unwritten slot points depends on the compiler. In the tested clang build it points into `_imaging.so`, and a larger count returned thousands of pointers into libpython, libc and libtiff, which defeats address-space randomisation for anyone chaining another bug. When the application preserves metadata with `tiffinfo=im.tag_v2`, a 64-bit entry turns ActiveArea into a 16-byte read from an address the attacker chooses. All observed memory errors are reads; no out-of-bounds write was found.\n\nOther branches of the same loop share the defect and are reachable from crafted TIFFs or from EXIF in JPEG/PNG/WebP re-saved as compressed TIFF. These are the `bytes`, RATIONAL-tuple and typed-tuple branches, and a BYTE-typed ColorMap. The file-reachable forwarding was introduced in `a3d45e9cefd0` (\"Added custom int and float TIFF tags\") and first released in 5.4.0.\n\n## Details\n\n`PyImaging_LibTiffEncoderNew` (`src/encode.c:777-1060`) chooses the argument\nlist it hands to the variadic `TIFFSetField()` from the *Python* value of each\ntag — scalar vs. tuple vs. `bytes`, and a type that is either autodetected or\ncopied from the source file's IFD — and assumes that its own\n`ImagingLibTiffMergeFieldInfo()` registration defines how libtiff will read the\n`va_list`. It does not: libtiff keeps its built-in definition for every tag it\nalready knows, and core tags are never registered at all. libtiff therefore\nconsumes the varargs as `(count, pointer)`, `(pointer)` or three pointers\naccording to *its* field table, whatever Pillow actually pushed. The encoder\nnever consults that table (`TIFFFindField` / `TIFFFieldPassCount` /\n`TIFFFieldWriteCount` / `TIFFFieldDataType`) before the call.\n\nThe most severe instance, and the one the attached reproducer exercises: when a\nTIFF is re-saved through the libtiff writer, every tag of the source IFD that\nPillow does not recognise is forwarded to libtiff with `TIFFSetField()`. A\ncount-1 integer entry is passed as a single scalar vararg. If libtiff's own\nfield table defines that tag number as an *array* field, libtiff consumes the\nvarargs as `(count, pointer)` or `(pointer)` instead, so the attacker's integer\nbecomes either the number of bytes to copy from a never-written vararg slot, or\nthe source address itself. The same mismatch is reachable through the typed\ntuple branches (tuple length consumed as a pointer for fixed-count built-in\ntags such as `CFARepeatPatternDim`, even when the source file is well-formed),\nthe `bytes`, `TIFF_RATIONAL`-tuple and core-tag branches of the same loop;\nthose are listed\nunder *Other branches with the same defect* below and need to be covered by the\nsame fix.\n\nA 181-byte TIFF (16×16 RGB, one LZW strip, plus one extra IFD entry\n`50740 / LONG / count 1 / value 4096` — DNGPrivateData) opened with\n`Image.open()` and saved with a bare `im.save(buf, \"TIFF\")` produces a 4296-byte\noutput whose tag 50740 holds 4096 bytes that occur nowhere in the input; scanning\n`/proc/self/mem` finds them in the read-only data segment of `_imaging.so`.\nChanging the extra entry to `50829 / LONG / 1 / 64` (ActiveArea) makes the same\ntwo calls segfault in `memcpy(dst, src=0x40, n=16)` under\n`ImagingLibTiffSetField` ← `PyImaging_LibTiffEncoderNew`. An ASan build of the\nextensions reports `READ of size 4096` in `memcpy` called from\n`src/encode.c:1014` for the first input.\n\n### Code path\n\n`_save` copies all tags of the opened image into the libtiff attribute list.\nFor a tag that is neither in `LIBTIFF_CORE` nor in `TiffTags.TAGS_V2`, no type\nis recorded, so the value goes through untyped:\n\n```python\n# src/PIL/TiffImagePlugin.py:1782\nsupplied_tags = {**legacy_ifd, **getattr(im, \"tag_v2\", {})}\n…\n# :1959-1982\nfor tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n if tag not in TiffTags.LIBTIFF_CORE:\n …\n elif isinstance(value, (int, float, str, bytes)) or …:\n type = TiffTags.lookup(tag).type # None for 50740, 50829, …\n if type:\n types[tag] = type\n if tag not in atts and tag not in blocklist:\n …\n atts[tag] = value # plain int 4096\n```\n\n`compression` defaults to the source image's `im.info[\"compression\"]`\n(`TiffImagePlugin.py:1731-1746`), so a compressed source selects the libtiff\nwriter without the application asking for it.\n\nIn the encoder the untyped `int` is autodetected as `TIFF_LONG`\n(`src/encode.c:805-815`), the tag is \"registered\" as a scalar custom field, and\nthen set with one 32-bit vararg:\n\n```c\n// src/encode.c:844 (is_var_length = 0 → writecount 1, passcount 0)\nif (ImagingLibTiffMergeFieldInfo(&encoder->state, type, key_int, is_var_length)) {\n … continue;\n}\n…\n// src/encode.c:1013-1016\n} else if (type == TIFF_LONG || type == TIFF_IFD) {\n status = ImagingLibTiffSetField(\n &encoder->state, (ttag_t)key_int, (UINT32)PyLong_AsLong(value)\n );\n```\n\nThe registration does not do what the caller assumes. `TIFFMergeFieldInfo()`\nonly adds definitions for tags libtiff does not already know and returns 0\n(success) either way, so for tags in libtiff's built-in table the built-in\ndefinition stays in force. Queried against the linked libtiff 4.5.0 after\nexactly Pillow's merge call:\n\n```\n50740 DNGPrivateData type=BYTE passcount=1 writecount=-1 (unchanged)\n50829 ActiveArea type=LONG passcount=0 writecount=4 (unchanged)\n65000 CustomField type=LONG passcount=0 writecount=1 (Pillow's definition, tag was unknown)\n```\n\nlibtiff's setter for `FIELD_CUSTOM` tags then interprets the `va_list`\naccording to *its* definition: for `passcount=1` it reads `va_arg(ap, int)` as\nthe element count — the attacker's 4096 — allocates `count * size` bytes and\n`memcpy`s that many bytes from `va_arg(ap, void *)`, a slot\n`PyImaging_LibTiffEncoderNew` never wrote. On x86-64 that slot is register\n`rcx`; in this build it holds a pointer into `_imaging.so`'s `.rodata`, so the\ncopy succeeds deterministically (a value of 2 000 000 also succeeds and yields a\n2 000 200-byte output). `TIFFWriteDirectory` writes the copied bytes into the\noutput file as the tag's value. For fixed-count array fields (`passcount=0`,\n`writecount=4` for ActiveArea) there is no count argument, so the attacker's\nscalar is consumed as the `void *` source and 16 bytes are read from address\n64 → SIGSEGV.\n\nTags in libtiff 4.5's built-in table that Pillow forwards untyped and that take\nthe `(count, pointer)` form include DNGPrivateData 50740, CFAPattern 33422,\nCFAPlaneColor 50710, LinearizationTable 50712, BlackLevel 50714, WhiteLevel\n50717, ColorMatrix1/2 50721/50722, AsShotNeutral 50728, OriginalRawFileName/Data\n50827/50828, AsShotICCProfile 50831, CurrentICCProfile 50833. Fixed-count ones\n(pointer only → crash) include ActiveArea 50829, DNGVersion/DNGBackwardVersion\n50706/50707, DefaultScale/DefaultCropOrigin/DefaultCropSize 50718–50720,\nAsShotWhiteXY 50729, CFARepeatPatternDim 33421, BlackLevelRepeatDim 50713,\nImageLayer 34732, RawDataUniqueID 50781.\n\nWhen the application saves with `tiffinfo=im.tag_v2` (the documented way to\npreserve metadata), the file-declared type is honoured, and a `LONG8` entry\nreaches the `(uint64_t)PyLong_AsLongLong(value)` branch at\n`src/encode.c:1049-1052`. For ActiveArea that is a 16-byte read from a full\n64-bit attacker-chosen address into the output: a source entry\n`50829 / LONG8 / 1 /
` produced an output\nActiveArea of `(b'SECR', b'ET-T', b'OKEN', b'-012')` in testing.\n\nThe comment above `blocklist +=` at `TiffImagePlugin.py:1938-1944` already notes\nthat array tags with the wrong length \"will result in segfaults\"; the blocklist\ncovers only a handful of tags Pillow itself knows, not the ones libtiff knows and\nPillow does not. Forwarding of these tags was introduced in `a3d45e9cefd0`\n(\"Added custom int and float TIFF tags\", 2018-10-25), first released in 5.4.0;\nreleases 2.0.0–3.0.0 forwarded them as scalars too, until `9fa6634f4`\n(\"Partial fix for #1597\") limited `_save` to `LIBTIFF_CORE`.\n\n### Other branches with the same defect\n\nThe scalar-integer branch above is one of several places in the same loop\nwhere the call shape is derived from the Python value rather than from\nlibtiff's definition. All of the following were confirmed on this commit (stock\nbuild crashes and/or ASan reports, stack always `memcpy ← libtiff ←\nImagingLibTiffSetField src/libImaging/TiffDecode.c:934 ←\nPyImaging_LibTiffEncoderNew`), and all are reachable from a sub-1 KB crafted\nfile followed by an ordinary `save()`:\n\n* **Typed tuple → `(len, av)` for a fixed-count built-in field**\n (`src/encode.c:904-994`, the SHORT/LONG/SBYTE/SSHORT/SLONG/FLOAT/DOUBLE\n tuple branches all call `ImagingLibTiffSetField(..., len, av)`). For libtiff\n built-in fields with `passcount=0` and a fixed `writecount` —\n `CFARepeatPatternDim` 33421 (SHORT×2), `BlackLevelRepeatDim` 50713\n (SHORT×2), `ImageLayer` 34732 (LONG×2), `ActiveArea` 50829 (LONG×4),\n `MatrixWorldToScreen/Camera` 33305/33306 (FLOAT×16) — libtiff takes the\n *first* vararg as the source pointer, so the tuple length becomes the\n `memcpy` source address. A perfectly well-formed 206-byte 8×8 PackBits TIFF\n carrying `33421 / SHORT / 2 / (2, 2)` re-saved with a bare\n `im.save(out, \"TIFF\")` dies with SIGSEGV in `memcpy` (gdb: `rsi = 0x2`, the\n tuple length) on every run; so does\n `Image.new(\"L\", (8, 8)).save(buf, \"TIFF\", compression=\"packbits\",\n tiffinfo={33421: (2, 2)})`, and a JPEG whose EXIF carries the tag saved with\n `exif=im.getexif()` into a compressed TIFF. `bytes` values for built-in\n fixed-count BYTE tags (e.g. `DNGVersion` 50706, BYTE×4) crash the same way\n through the `(PyBytes_Size, PyBytes_AsString)` call at `src/encode.c:860-865`.\n The mirror image for `passcount=1` fields: `Photoshop` 34377 declared\n SHORT, count 1, value 60000 takes the scalar branch at `:1009-1012`, libtiff\n reads 60000 as the count and an unpassed vararg as the pointer, and the\n re-save *succeeds* with a 60162-byte output whose tag 34377 holds 60000 bytes\n of process memory (here C prototype strings from `_imaging.so`'s `.rodata`).\n* **`bytes` value → single `char *`** (`src/encode.c:805-815` autodetects\n `bytes` as `TIFF_ASCII`; `:1033-1042` passes `PyBytes_AsString(value)` alone).\n `ImageFileDirectory_v2._setitem` stores a BYTE-typed entry as bare `bytes`\n and an ASCII-typed one as `str` (encoded to `bytes` by `_save`), so the\n file's declared type selects this branch.\n - `ColorMap` (320) declared BYTE with 4 bytes of data in a non-palette\n PackBits TIFF: 320 is in `core_tags[]`, no type is consulted, the value\n skips the 768-tuple `TIFFTAG_COLORMAP` branch (`:871-895`) and libtiff reads\n three `uint16_t *` and copies `1 << BitsPerSample` shorts from each — a\n 512-byte over-read of the 5-byte Python bytes buffer, then dereference of\n two stale argument registers → SIGSEGV on every run of\n `Image.open(f); im.save(out, \"TIFF\")`. `COLORMAP` is not in `_save`'s\n `blocklist`.\n - `Photoshop` (34377) declared ASCII: `_save` copies value *and* type\n (`TiffImagePlugin.py:1802-1818`; only `IPTC_NAA_CHUNK` has a BYTE/UNDEFINED\n type guard there, from commit 2245df0a — `PHOTOSHOP_CHUNK` and `XMP` do\n not). libtiff's definition is BYTE, `passcount=1`, so the low 32 bits of\n the `char *` become the byte count for a `memcpy` from a stale register\n (ASan: `READ of size 3518384`; stock: SIGSEGV in about half of runs,\n otherwise `RuntimeError`). `ICCProfile` (34675) declared ASCII behaves the\n same way via `im.info[\"icc_profile\"]` → `ifd[ICCPROFILE]`\n (`TiffImagePlugin.py:1822-1824`).\n* **`TIFF_RATIONAL` tuple → array with no count** (`src/encode.c:995-1006`;\n compare the `TIFF_FLOAT` branch at `:969-981`, which passes `len, av`).\n - For a private tag (e.g. 50000) Pillow itself registers the field with\n `passcount=1` (`TiffDecode.c:904-908`), so libtiff takes the low 32 bits of\n `av` as the element count and the next stale vararg as the source:\n `heap-buffer-overflow READ of size 119872 … right of 8-byte region` under\n ASan (`119872 == 4 * low32(av)`), SIGSEGV in ~4 of 5 stock runs. Reached by\n a JPEG whose EXIF carries one private RATIONAL×2 tag saved with\n `im.save(out, \"TIFF\", compression=\"tiff_lzw\", exif=im.info[\"exif\"])`, or a\n TIFF re-saved with `tiffinfo=im.tag_v2` (`_setitem` auto-types a tuple of\n `IFDRational` as RATIONAL, `TiffImagePlugin.py:689-701`; `types[tag] =\n ifd.tagtype[tag]` at `:1968`).\n - For libtiff's built-in fixed-count float tags `WhitePoint` (318, 2),\n `PrimaryChromaticities` (319, 6) and `YCbCrCoefficients` (529, 3), which\n `_save` forwards from any source TIFF and types RATIONAL via\n `TiffTags.lookup`, the pointer-only convention happens to match, but\n libtiff copies its *fixed* count from an array that is only as long as the\n file declared: a PackBits TIFF with `PrimaryChromaticities` count 1 and a\n bare `im.save(out, \"TIFF\")` gives `READ of size 24 … right of 4-byte\n region`, and the over-read floats are written to tag 319 of the output.\n `REFERENCEBLACKWHITE`/`TRANSFERFUNCTION` are blocklisted at\n `TiffImagePlugin.py:1941-1952` for exactly this reason; 318/319/529 are not.\n* **Core tag reached as a scalar via a stale file-supplied type.** With the\n re-save idiom `im.save(out, \"TIFF\", tiffinfo=im.tag_v2)`, `_save` copies\n `ifd.tagtype[key] = info.tagtype[key]` for every key\n (`TiffImagePlugin.py:1768-1776`) and later assigns its own values to some of\n the same tags without resetting the type (`ifd[COLORMAP] = colormap` at\n `:1870-1877`; likewise BitsPerSample, SamplesPerPixel, ExtraSamples,\n ICCProfile, …). For a 418-byte PackBits *palette* TIFF whose ColorMap entry\n is declared BYTE, `_setitem` (`:751-770`) truncates `_save`'s 768-entry\n palette to a single `int` (with only a \"too many entries\" warning), the\n encoder receives `(320, 0)`, autodetects `TIFF_LONG`, and\n `src/encode.c:1013-1016` calls `TIFFSetField(TIFFTAG_COLORMAP, (UINT32)0)` —\n libtiff reads three `uint16_t *` and faults at an address ≤ 65280 on every\n run. The stale-tagtype copy is a Python-side hygiene bug in its own right,\n but its memory-safety consequence exists only because the encoder lets a\n non-tuple value reach `TIFFSetField(COLORMAP)`.\n\n## Impact\n\nAny service that accepts a TIFF and writes it back out as TIFF through libtiff —\nwhich happens with a plain `im.save(fp, \"TIFF\")` whenever the uploaded file is\ncompressed, or whenever a `compression=` other than raw is requested — returns\nto the uploader, inside one metadata tag of the produced file, an\nattacker-chosen number of bytes read from a stale pointer in the worker process,\ndeterministically and without retries. Which memory that is depends on the\ncompiler's register allocation for `PyImaging_LibTiffEncoderNew`: in this clang\nbuild the pointer is `_imaging.so + 0x1588`, so a request for 262 144 bytes\n(which succeeded every time in testing, as did 2 000 000) covers the\nextension's `.rodata`, `.data.rel.ro`/GOT and `.data` and returned 2 800+\nresolved pointers into libpython, libc, libtiff, libjpeg and others — a complete\nASLR bypass for chaining with any other memory bug; a gcc build of the same\nsource left a different, unrelated address in that slot. The fixed-count\nvariant kills the worker process with SIGSEGV from a 181-byte upload on every\nrequest. With `tiffinfo=im.tag_v2` the source\naddress is fully attacker-chosen (16-byte arbitrary read into the output for\nActiveArea, or a crash). No non-default Pillow setting is involved; the only\nprecondition is TIFF-to-TIFF re-encoding.\n\nThe other branches add further deterministic process-kill inputs (BYTE-typed\nColorMap in any RGB or palette TIFF; `thumbnail()` keeps `tag_v2`, so\nopen→thumbnail→save-as-TIFF is affected too) and extend the reach to\nJPEG/PNG/WebP uploads whose EXIF is preserved with `exif=` when converting to\nlibtiff-compressed TIFF. All the memory errors are reads; libtiff sizes its\ndestination buffers with the same count it copies, so no out-of-bounds write\nwas found.\n\n## Reproducing\n\n`repro` is one Python script, run against the stock build with the installed `PIL`. It needs no compiler and no extra files.\n\n`upload(tag, value)` has Pillow write a 16×16 red RGB image as an LZW-compressed TIFF, then appends a new IFD holding the same entries plus one extra ` / LONG / count 1 / value` (kept in tag order) and points the header at it — a 356-byte input on this tree. The source is LZW-compressed on purpose. A bare `im.save(out, \"TIFF\")` keeps the source's compression, so the save goes through the libtiff writer even though the application passes no arguments. `resave()` is the whole \"application\": `Image.open(BytesIO(src)); im.save(out, \"TIFF\")`.\n\nThe script runs three cases:\n\n1. **Control, tag 65000 = 4096.** libtiff does not know this tag, so the file is re-saved correctly and the output tag reads back as the integer `4096`. This is what should happen to every tag.\n2. **Leak, DNGPrivateData 50740 = 65536.** libtiff's built-in definition of this tag is a counted BYTE array. It reads Pillow's single scalar as the *count* and takes the data pointer from a vararg slot Pillow never wrote. The output tag comes back as `bytes` of length 65536 that do not occur in the input. The script prints the first 32 of them and the file offset at which they occur in `_imaging.so` on disk. On this build they always do (offset `0x57588`): the stale pointer aims into the extension's read-only data.\n3. **Wild read, ActiveArea 50829 = 64.** libtiff defines this tag as a fixed LONG[4] array with no count, so it uses the scalar 64 as the *source address*. The script re-runs itself as a child process (`repro child`, core dumps disabled) to do this re-save, and the child exits with `-11`, meaning SIGSEGV.\n\nExpected output on the affected tree:\n\n```\ncontrol tag 65000=4096 -> 4096\nleak tag 50740=65536 from a 356-byte upload -> 65536 bytes not present in input, head a661fcffc661fcff3360fcff7b61fcffd361fcff085ffcffe061fcff3360fcff\n leaked bytes found in _imaging.so at file offset 0x57588\ncrash tag 50829=64 -> child exit -11 (SIGSEGV)\nBUG\n```\n\nThe script prints `BUG` if case 2 leaks or case 3 crashes. Both are deterministic on this build. A fixed Pillow prints `OK`: the tag does not come back as 65536 bytes of foreign data (it reads back as `None`, or `save()` raises `ValueError`/`RuntimeError`, which the script catches), and the child exits with something other than SIGSEGV (0, or 1 if `save()` raised).\n\nThe other branches can be checked by hand on the stock build in the same way\n(each as `Image.open(f); im.save(BytesIO(), \"TIFF\")` in a child process):\na PackBits RGB TIFF with an extra IFD entry `320 / BYTE / 4 / \"ABCD\"` →\nSIGSEGV every run; a PackBits palette TIFF whose ColorMap is `320 / BYTE / 2`\nsaved with `tiffinfo=im.tag_v2` → SIGSEGV every run; a Pillow-written JPEG\nwhose EXIF holds tag 50000 = RATIONAL×2 saved with\n`compression=\"tiff_lzw\", exif=im.info[\"exif\"]` → SIGSEGV in most runs,\n`RuntimeError` (\"Failed to allocate memory for custom tag binary object\n(-N elements of 4 bytes each)\") in the rest.\n\n## Suggested fix\n\nThe encoder must not guess the calling convention; it has to ask libtiff what\nthe tag's effective definition is *after* `ImagingLibTiffMergeFieldInfo` and\nmarshal accordingly, or refuse. The attached patch does this for non-core tags:\n\n* a new `ImagingLibTiffFindField()` in `src/libImaging/TiffDecode.c` (declared\n in `TiffDecode.h`, next to the other libtiff wrappers) returns libtiff's\n `TIFFFindField` type, `passcount` and `writecount` for a tag;\n* in `PyImaging_LibTiffEncoderNew` (`src/encode.c`), for a non-core tag that\n libtiff already knows, Pillow adopts libtiff's type when it has none of its\n own, and after `ImagingLibTiffMergeFieldInfo` a new `tiff_field_matches()`\n checks the value against libtiff's definition: a scalar only for a\n single-value field whose C argument type (int / `uint64_t` / double /\n `char *`) matches, an array only with libtiff's element size and, for\n fixed-count fields, exactly `writecount` elements. Anything else raises\n `ValueError(\"Tag value does not match libtiff field definition\")` instead of\n reaching `TIFFSetField` — this covers the reproducer's DNGPrivateData and\n ActiveArea inputs, ASCII-typed Photoshop, short PrimaryChromaticities and the\n `LONG8` `tiffinfo=` variant;\n* every tuple/`bytes` branch now goes through `set_tiff_array_field()`, which\n passes `len` only when the field has `passcount` set; this also fixes the\n `TIFF_RATIONAL` branch, which never passed a count (and extends it to\n `TIFF_SRATIONAL`);\n* `TIFF_UNDEFINED` fields are registered as variable-length, matching how their\n `bytes` values are passed;\n* `Tests/test_file_libtiff.py` gains a regression test (scalar DNGPrivateData,\n scalar and 2-tuple ActiveArea rejected; well-formed values round-trip).\n\nCore tags and tags libtiff does not know keep their current behaviour. With the\npatch applied to a clean checkout and rebuilt, the reproducer no longer\ntriggers and the test suite passes. It is not a complete fix, however: the\nwhole check sits inside `if (!is_core_tag)`, so the BYTE-typed ColorMap variant\nstays open. The PackBits RGB TIFF carrying `320 / BYTE / 4`, re-saved with a\nbare `save()`, no longer segfaults at this point but writes 1536 bytes —\nover-read from the 5-byte `bytes` object and two stale varargs, including 22\nlive heap pointers — into the output ColorMap, which is still a disclosure.\nBefore merging, it needs a guard that accepts `TIFFTAG_COLORMAP` only through\nthe existing 768-tuple branch, never through the scalar or `bytes` branches,\nand ideally the same shape check for the other core tags libtiff special-cases\nin `_TIFFVSetField` (YCbCrSubsampling 530, TransferFunction 301,\nReferenceBlackWhite 532).\n\nNarrower Python-side mitigations in `_save` close individual entry points but\nnot the class, and are worth adding alongside the C-side check: drop tags\nwith `TiffTags.lookup(tag).type is None` from `supplied_tags` (closes the `Image.open()→save()` path shown here but not\n`tiffinfo=` callers); remove `COLORMAP` from `supplied_tags` next to\n`EXIFIFD`/`SAMPLEFORMAT` and extend the `IPTC_NAA_CHUNK` BYTE/UNDEFINED type\nguard to `PHOTOSHOP_CHUNK` and `XMP`; add 318/319/529 to the blocklist next to\n`REFERENCEBLACKWHITE`; and have `_save` `ifd.tagtype.pop(tag, None)` before\nassigning its own ColorMap/BitsPerSample/ICCProfile/… values so a\n`tiffinfo=`-imported type cannot reshape them. The C-side check is the fix to\nrely on.\n\n## The patch\n\nPass TIFF tag values to libtiff according to its own field definition, and raise ValueError when they do not fit\n\nApplies to `src/encode.c` 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/Tests/test_file_libtiff.py b/Tests/test_file_libtiff.py\nindex 3d804ea4a..adc4633e4 100644\n--- a/Tests/test_file_libtiff.py\n+++ b/Tests/test_file_libtiff.py\n@@ -395,6 +395,43 @@ class TestFileLibTiff(LibTiffTestCase):\n with pytest.raises(ValueError, match=\"Incorrect tag value type\"):\n im.save(out, tiffinfo=ifd)\n \n+ @pytest.mark.parametrize(\n+ \"tag, value\",\n+ (\n+ # DNGPrivateData is a BYTE array with a count to libtiff\n+ (50740, 4096),\n+ # ActiveArea is a LONG array of four values to libtiff\n+ (50829, 64),\n+ (50829, (0, 0)),\n+ ),\n+ )\n+ def test_libtiff_field_definition(\n+ self, tag: int, value: Any, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n+ ) -> None:\n+ monkeypatch.setattr(TiffImagePlugin, \"WRITE_LIBTIFF\", True)\n+\n+ ifd = TiffImagePlugin.ImageFileDirectory_v2()\n+ ifd[tag] = value\n+ ifd.tagtype[tag] = TiffTags.LONG\n+\n+ out = tmp_path / \"temp.tif\"\n+ im = Image.new(\"L\", (1, 1))\n+ with pytest.raises(ValueError, match=\"libtiff field definition\"):\n+ im.save(out, tiffinfo=ifd)\n+\n+ # Values that match the libtiff definition are written\n+ ifd = TiffImagePlugin.ImageFileDirectory_v2()\n+ ifd[50740] = b\"private\"\n+ ifd.tagtype[50740] = TiffTags.BYTE\n+ ifd[50829] = (0, 0, 1, 1)\n+ ifd.tagtype[50829] = TiffTags.LONG\n+ im.save(out, tiffinfo=ifd)\n+\n+ with Image.open(out) as reloaded:\n+ assert isinstance(reloaded, TiffImagePlugin.TiffImageFile)\n+ assert reloaded.tag_v2[50740] == b\"private\"\n+ assert reloaded.tag_v2[50829] == (0, 0, 1, 1)\n+\n def test_inknames_tag(\n self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path\n ) -> None:\ndiff --git a/src/encode.c b/src/encode.c\nindex 9e19e9002..cbbd09f0d 100644\n--- a/src/encode.c\n+++ b/src/encode.c\n@@ -675,6 +675,106 @@ PyImaging_ZipEncoderNew(PyObject *self, PyObject *args) {\n \n #include \n \n+/* C type that TIFFSetField reads from its arguments for a single value of\n+ the given type: int, uint64_t, double or char * */\n+static int\n+tiff_scalar_arg_type(TIFFDataType type) {\n+ switch (type) {\n+ case TIFF_BYTE:\n+ case TIFF_SBYTE:\n+ case TIFF_UNDEFINED:\n+ case TIFF_SHORT:\n+ case TIFF_SSHORT:\n+ case TIFF_LONG:\n+ case TIFF_SLONG:\n+ case TIFF_IFD:\n+ return 1;\n+ case TIFF_LONG8:\n+ case TIFF_SLONG8:\n+ case TIFF_IFD8:\n+ return 2;\n+ case TIFF_FLOAT:\n+ case TIFF_DOUBLE:\n+ case TIFF_RATIONAL:\n+ case TIFF_SRATIONAL:\n+ return 3;\n+ case TIFF_ASCII:\n+ return 4;\n+ default:\n+ return 0;\n+ }\n+}\n+\n+/* Size of the values that TIFFSetField copies from an array of the given\n+ type. libtiff stores custom RATIONAL arrays as floats. */\n+static int\n+tiff_array_item_size(TIFFDataType type) {\n+ switch (type) {\n+ case TIFF_BYTE:\n+ case TIFF_SBYTE:\n+ case TIFF_UNDEFINED:\n+ case TIFF_ASCII:\n+ return 1;\n+ case TIFF_SHORT:\n+ case TIFF_SSHORT:\n+ return 2;\n+ case TIFF_LONG:\n+ case TIFF_SLONG:\n+ case TIFF_IFD:\n+ case TIFF_FLOAT:\n+ case TIFF_RATIONAL:\n+ case TIFF_SRATIONAL:\n+ return 4;\n+ case TIFF_DOUBLE:\n+ case TIFF_LONG8:\n+ case TIFF_SLONG8:\n+ case TIFF_IFD8:\n+ return 8;\n+ default:\n+ return 0;\n+ }\n+}\n+\n+/* Check that a value which will be passed to TIFFSetField as a single value\n+ of the given type, or as an array of len values, matches the definition\n+ of the field that libtiff will use. */\n+static int\n+tiff_field_matches(\n+ TIFFDataType field_type,\n+ int passcount,\n+ int writecount,\n+ TIFFDataType type,\n+ int is_var_length,\n+ Py_ssize_t len\n+) {\n+ if (field_type == TIFF_ASCII && !passcount) {\n+ // TIFFSetField reads a null-terminated string\n+ return !is_var_length && type == TIFF_ASCII;\n+ }\n+ if (!is_var_length) {\n+ // TIFFSetField reads a single value\n+ return !passcount && writecount == 1 && tiff_scalar_arg_type(type) &&\n+ tiff_scalar_arg_type(type) == tiff_scalar_arg_type(field_type);\n+ }\n+ // TIFFSetField reads a pointer to the values, preceded by the number of\n+ // values if passcount is set. Otherwise, the number of values is fixed.\n+ if (!tiff_array_item_size(type) ||\n+ tiff_array_item_size(type) != tiff_array_item_size(field_type)) {\n+ return 0;\n+ }\n+ return passcount || (writecount > 1 && len == writecount);\n+}\n+\n+static int\n+set_tiff_array_field(\n+ ImagingCodecState state, int key, int passcount, Py_ssize_t len, void *av\n+) {\n+ if (passcount) {\n+ return ImagingLibTiffSetField(state, (ttag_t)key, len, av);\n+ }\n+ return ImagingLibTiffSetField(state, (ttag_t)key, av);\n+}\n+\n PyObject *\n PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n ImagingEncoderObject *encoder;\n@@ -689,7 +789,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n PyObject *key, *value;\n Py_ssize_t pos = 0;\n int key_int, status, is_core_tag, is_var_length, num_core_tags, i;\n- TIFFDataType type = TIFF_NOTYPE;\n+ int is_known_tag, passcount, writecount;\n+ TIFFDataType type = TIFF_NOTYPE, field_type;\n // This list also exists in TiffTags.py\n const int core_tags[] = {256, 257, 258, 259, 262, 263, 266, 269, 274, 277,\n 278, 280, 281, 282, 283, 284, 286, 287, 296, 297,\n@@ -777,6 +878,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n status = 0;\n is_core_tag = 0;\n is_var_length = 0;\n+ is_known_tag = 0;\n+ passcount = 1;\n type = TIFF_NOTYPE;\n \n for (i = 0; i < num_core_tags; i++) {\n@@ -800,6 +903,15 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n }\n Py_DECREF(tag_type);\n }\n+\n+ // libtiff uses its own definition for tags that it already knows,\n+ // rather than the field registered below.\n+ is_known_tag = ImagingLibTiffFindField(\n+ &encoder->state, key_int, &field_type, &passcount, &writecount\n+ );\n+ if (is_known_tag && type == TIFF_NOTYPE) {\n+ type = field_type;\n+ }\n }\n \n if (type == TIFF_NOTYPE) {\n@@ -838,7 +950,7 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n \n if (!is_core_tag) {\n // Register field for non core tags.\n- if (type == TIFF_BYTE) {\n+ if (type == TIFF_BYTE || type == TIFF_UNDEFINED) {\n is_var_length = 1;\n }\n if (ImagingLibTiffMergeFieldInfo(\n@@ -847,6 +959,28 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n Py_DECREF(item);\n continue;\n }\n+\n+ if (is_known_tag) {\n+ // Check that the value will be passed to TIFFSetField in the\n+ // form that libtiff's definition of the tag expects.\n+ Py_ssize_t len = 0;\n+ if (PyTuple_Check(value)) {\n+ len = PyTuple_Size(value);\n+ } else if (PyBytes_Check(value)) {\n+ len = PyBytes_Size(value);\n+ }\n+ if (!tiff_field_matches(\n+ field_type, passcount, writecount, type, is_var_length, len\n+ )) {\n+ Py_DECREF(encoder);\n+ Py_DECREF(item);\n+ PyErr_SetString(\n+ PyExc_ValueError,\n+ \"Tag value does not match libtiff field definition\"\n+ );\n+ return NULL;\n+ }\n+ }\n }\n \n if (type == TIFF_BYTE || type == TIFF_UNDEFINED ||\n@@ -857,9 +991,10 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n PyErr_SetString(PyExc_ValueError, \"Incorrect tag value type\");\n return NULL;\n }\n- status = ImagingLibTiffSetField(\n+ status = set_tiff_array_field(\n &encoder->state,\n- (ttag_t)key_int,\n+ key_int,\n+ passcount,\n PyBytes_Size(value),\n PyBytes_AsString(value)\n );\n@@ -909,8 +1044,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (UINT16)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -922,8 +1057,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (UINT32)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -935,8 +1070,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT8)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -948,8 +1083,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT16)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -961,8 +1096,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (INT32)PyLong_AsLong(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -974,8 +1109,8 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n@@ -987,12 +1122,12 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status = ImagingLibTiffSetField(\n- &encoder->state, (ttag_t)key_int, len, av\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n );\n free(av);\n }\n- } else if (type == TIFF_RATIONAL) {\n+ } else if (type == TIFF_RATIONAL || type == TIFF_SRATIONAL) {\n FLOAT32 *av;\n /* malloc check ok, calloc checks for overflow */\n av = calloc(len, sizeof(FLOAT32));\n@@ -1000,8 +1135,9 @@ PyImaging_LibTiffEncoderNew(PyObject *self, PyObject *args) {\n for (i = 0; i < len; i++) {\n av[i] = (FLOAT32)PyFloat_AsDouble(PyTuple_GetItem(value, i));\n }\n- status =\n- ImagingLibTiffSetField(&encoder->state, (ttag_t)key_int, av);\n+ status = set_tiff_array_field(\n+ &encoder->state, key_int, passcount, len, av\n+ );\n free(av);\n }\n }\ndiff --git a/src/libImaging/TiffDecode.c b/src/libImaging/TiffDecode.c\nindex 8a57c523d..e14dd50bb 100644\n--- a/src/libImaging/TiffDecode.c\n+++ b/src/libImaging/TiffDecode.c\n@@ -923,6 +923,29 @@ ImagingLibTiffMergeFieldInfo(\n return TIFFMergeFieldInfo(clientstate->tiff, info, n);\n }\n \n+int\n+ImagingLibTiffFindField(\n+ ImagingCodecState state,\n+ int key,\n+ TIFFDataType *field_type,\n+ int *passcount,\n+ int *writecount\n+) {\n+ // Look up libtiff's own definition of a tag. TIFFMergeFieldInfo does not\n+ // replace the definition of a tag that libtiff already knows, so this is\n+ // what TIFFSetField will use for such tags.\n+ TIFFSTATE *clientstate = (TIFFSTATE *)state->context;\n+ const TIFFField *fip = TIFFFindField(clientstate->tiff, (ttag_t)key, TIFF_ANY);\n+\n+ if (!fip) {\n+ return 0;\n+ }\n+ *field_type = TIFFFieldDataType(fip);\n+ *passcount = TIFFFieldPassCount(fip);\n+ *writecount = TIFFFieldWriteCount(fip);\n+ return 1;\n+}\n+\n int\n ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...) {\n // after tif_dir.c->TIFFSetField.\ndiff --git a/src/libImaging/TiffDecode.h b/src/libImaging/TiffDecode.h\nindex c31d0b9d8..ee1e4a5f9 100644\n--- a/src/libImaging/TiffDecode.h\n+++ b/src/libImaging/TiffDecode.h\n@@ -41,6 +41,14 @@ ImagingLibTiffMergeFieldInfo(\n ImagingCodecState state, TIFFDataType field_type, int key, int is_var_length\n );\n extern int\n+ImagingLibTiffFindField(\n+ ImagingCodecState state,\n+ int key,\n+ TIFFDataType *field_type,\n+ int *passcount,\n+ int *writecount\n+);\n+extern int\n ImagingLibTiffSetField(ImagingCodecState state, ttag_t tag, ...);\n \n /*\n```\n\n## The reproducer\n\n
\nReproducer — repro, 2,545 bytes (click to open)\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# Pillow libtiff writer: a count-1 integer tag copied from the source TIFF is passed to\n# TIFFSetField() as one scalar vararg; for tags libtiff itself defines as arrays it is\n# consumed as a byte COUNT (DNGPrivateData 50740 -> N bytes of process memory written\n# into the output) or as a POINTER (ActiveArea 50829 -> SIGSEGV). Application side is\n# the ordinary \"open upload, save as TIFF\" (compression is inherited -> libtiff writer).\nimport io, struct, subprocess, sys, resource\nfrom PIL import Image\n\ndef upload(tag, value):\n \"\"\"Pillow-written 16x16 LZW TIFF, plus one extra IFD entry LONG[1] = value.\"\"\"\n b = io.BytesIO()\n Image.new(\"RGB\", (16, 16), \"red\").save(b, \"TIFF\", compression=\"tiff_lzw\")\n data = bytearray(b.getvalue()); assert data[:4] == b\"II*\\0\"\n (off,) = struct.unpack_from(\" 1 and sys.argv[1] == \"child\":\n resource.setrlimit(resource.RLIMIT_CORE, (0, 0))\n resave(upload(50829, 64)); sys.exit(0)\n\nprint(\"PIL core:\", Image.core.__file__)\nctl = Image.open(io.BytesIO(resave(upload(65000, 4096)))).tag_v2.get(65000)\nprint(\"control tag 65000=4096 ->\", repr(ctl))\n\nN = 65536\nsrc = upload(50740, N)\ntry:\n v = Image.open(io.BytesIO(resave(src))).tag_v2.get(50740)\nexcept (ValueError, RuntimeError) as e: # a fixed Pillow may reject the tag\n v = e\nleak = isinstance(v, bytes) and len(v) == N and v[:64] not in src\nprint(f\"leak tag 50740={N} from a {len(src)}-byte upload ->\",\n f\"{len(v)} bytes not present in input, head {v[:32].hex()}\" if leak else repr(v)[:80])\nif leak:\n so = open(Image.core.__file__, \"rb\").read()\n print(\" leaked bytes found in _imaging.so at file offset\", hex(so.find(v[:256])))\n\nrc = subprocess.call([sys.executable, __file__, \"child\"])\ncrash = rc in (-11, 139)\nprint(\"crash tag 50829=64 -> child exit\", rc, \"(SIGSEGV)\" if crash else \"\")\nprint(\"BUG\" if leak or crash else \"OK\")\n```\n
\n\n---","affected":[{"package":{"name":"pillow","ecosystem":"PyPI","purl":"pkg:pypi/pillow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"5.4.0"},{"fixed":"13.0.0"}]}],"versions":["10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","5.4.0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"],"database_specific":{"last_known_affected_version_range":"<= 12.3.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-xvh5-g6gv-559v/GHSA-xvh5-g6gv-559v.json"}}],"references":[{"type":"WEB","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-xvh5-g6gv-559v"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-112902"},{"type":"WEB","url":"https://github.com/github/advisory-database/pull/10903"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/pull/10227"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/570df030c0d30c2bf5336b2ab59f3902d8ab0b6d"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/commit/af56ace297b0991d02988fcc2ea933e4824b4fb3"},{"type":"PACKAGE","url":"https://github.com/python-pillow/Pillow"},{"type":"WEB","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"}],"database_specific":{"cwe_ids":["CWE-686"],"github_reviewed":true,"github_reviewed_at":"2026-10-21T21:29:16Z","nvd_published_at":"2026-11-05T21:18:36Z","severity":"HIGH"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:H/SC:N/SI:N/SA:N"}]} \ No newline at end of file diff --git a/data/PyPI/MAL-2026-19116.json b/data/PyPI/MAL-2026-19116.json new file mode 100644 index 0000000000..58ef04e9d1 --- /dev/null +++ b/data/PyPI/MAL-2026-19116.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"MAL-2026-19116","published":"2026-11-11T08:34:00Z","modified":"2026-11-11T09:15:03.056260589Z","summary":"Malicious code in humbility-shared (PyPI)","details":"\n---\n_-= Per source details. Do not edit below this line.=-_\n\n## Source: kam193 (bad6120ea5f69f851eafe78dfbb832979077fa6c0175c2bdda88bf073b8ad487)\nInstalling the package or importing the module exfiltrates basic information about the host, and the package has no other purpose.\n\n\n---\n\nCategory: PROBABLY_PENTEST - Packages looking like typical pentest packages, but also anything that looks like testing, exploring pre-prepared kits, research & co, with clearly low-harm possibilities.\n\n\nCampaign: GENERIC-standard-pypi-install-pentest\n\n\nReasons (based on the campaign):\n\n\n - The package contains code to exfiltrate basic data from the system, like IP or username. It has a limited risk.\n\n\n - The package overrides the install command in setup.py to execute malicious code during installation.\n","affected":[{"package":{"name":"humbility-shared","ecosystem":"PyPI","purl":"pkg:pypi/humbility-shared"},"versions":["0.0.1","0.0.2"],"database_specific":{"source":"https://github.com/ossf/malicious-packages/blob/main/osv/malicious/pypi/humbility-shared/MAL-2026-19116.json"}}],"references":[{"type":"WEB","url":"https://bad-packages.kam193.eu/pypi/package/humbility-shared"}],"database_specific":{"malicious-packages-origins":[{"id":"pypi/GENERIC-standard-pypi-install-pentest/humbility-shared","import_time":"2026-11-11T09:13:15.507864771Z","modified_time":"2026-11-11T08:34:00.054982Z","sha256":"bad6120ea5f69f851eafe78dfbb832979077fa6c0175c2bdda88bf073b8ad487","source":"kam193","versions":["0.0.1","0.0.2"]}]},"credits":[{"name":"Kamil Mańkowski (kam193)","contact":["https://github.com/kam193","https://bad-packages.kam193.eu/"],"type":"REPORTER"}]} \ No newline at end of file diff --git a/data/PyPI/PYSEC-2026-4363.json b/data/PyPI/PYSEC-2026-4363.json new file mode 100644 index 0000000000..67ced23ef5 --- /dev/null +++ b/data/PyPI/PYSEC-2026-4363.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"PYSEC-2026-4363","published":"2026-11-05T21:18:36.713Z","modified":"2026-11-11T10:00:02.351084890Z","aliases":["CVE-2026-112902","GHSA-xvh5-g6gv-559v"],"details":"Pillow is a Python imaging library. In versions through 12.3.0, the libtiff TIFF encoder can pass arguments to  TIFFSetField()  that do not match libtiff’s definition of the corresponding tag. When an application saves attacker-controlled image metadata through this encoder, crafted tag values can cause invalid memory reads, copying process memory into the encoded output or crashing the process. Applications that return the encoded output to an attacker can disclose that memory. The issue is reachable when re-saving compressed TIFF images and through certain conversions that preserve EXIF metadata into compressed TIFF output. This vulnerability is fixed in version 13.0.0.","affected":[{"package":{"name":"pillow","ecosystem":"PyPI","purl":"pkg:pypi/pillow"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"13.0.0"}]}],"versions":["1.0","1.1","1.2","1.3","1.4","1.5","1.6","1.7.0","1.7.1","1.7.2","1.7.3","1.7.4","1.7.5","1.7.6","1.7.7","1.7.8","10.0.0","10.0.1","10.1.0","10.2.0","10.3.0","10.4.0","11.0.0","11.1.0","11.2.1","11.3.0","12.0.0","12.1.0","12.1.1","12.2.0","12.3.0","2.0.0","2.1.0","2.2.0","2.2.1","2.2.2","2.3.0","2.3.1","2.3.2","2.4.0","2.5.0","2.5.1","2.5.2","2.5.3","2.6.0","2.6.1","2.6.2","2.7.0","2.8.0","2.8.1","2.8.2","2.9.0","3.0.0","3.1.0","3.1.0.rc1","3.1.0rc1","3.1.1","3.1.2","3.2.0","3.3.0","3.3.1","3.3.2","3.3.3","3.4.0","3.4.1","3.4.2","4.0.0","4.1.0","4.1.1","4.2.0","4.2.1","4.3.0","5.0.0","5.1.0","5.2.0","5.3.0","5.4.0","5.4.0.dev0","5.4.1","6.0.0","6.1.0","6.2.0","6.2.1","6.2.2","7.0.0","7.1.0","7.1.1","7.1.2","7.2.0","8.0.0","8.0.1","8.1.0","8.1.1","8.1.2","8.2.0","8.3.0","8.3.1","8.3.2","8.4.0","9.0.0","9.0.1","9.1.0","9.1.1","9.2.0","9.3.0","9.4.0","9.5.0"],"ecosystem_specific":{},"database_specific":{"source":"https://github.com/pypa/advisory-database/blob/main/vulns/pillow/PYSEC-2026-4363.yaml"}}],"references":[{"type":"ADVISORY","url":"https://github.com/python-pillow/Pillow/releases/tag/13.0.0"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/commit/570df030c0d30c2bf5336b2ab59f3902d8ab0b6d"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/pull/10227"},{"type":"FIX","url":"https://github.com/python-pillow/Pillow/security/advisories/GHSA-xvh5-g6gv-559v"}],"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0269.json b/data/crates.io/RUSTSEC-2026-0269.json index 72adb5e52c..587c17eb2d 100644 --- a/data/crates.io/RUSTSEC-2026-0269.json +++ b/data/crates.io/RUSTSEC-2026-0269.json @@ -1 +1 @@ -{"schema_version":"1.9.0","id":"RUSTSEC-2026-0269","published":"2026-08-20T12:00:00Z","modified":"2026-10-24T06:25:41Z","aliases":["CVE-2026-115392","GHSA-vqjp-4c8c-hfgg"],"summary":"Filesystem sandbox escape when paths or symlinks contain trailing slashes","details":"This is an entry in the RustSec database for the Wasmtime security advisory\nlocated at\nhttps://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-vqjp-4c8c-hfgg\nFor more information see the GitHub-hosted security advisory.","affected":[{"package":{"name":"wasmtime","ecosystem":"crates.io","purl":"pkg:cargo/wasmtime"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"24.0.13"},{"introduced":"25.0.0"},{"fixed":"36.0.14"},{"introduced":"37.0.0"},{"fixed":"46.0.3"},{"introduced":"47.0.0"},{"fixed":"47.0.4"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":[],"cvss":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0269.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/wasmtime"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0269.html"},{"type":"WEB","url":"https://github.com/bytecodealliance/wasmtime/pull/14182"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L"}]} \ No newline at end of file +{"schema_version":"1.9.0","id":"RUSTSEC-2026-0269","published":"2026-08-20T12:00:00Z","modified":"2026-11-11T08:57:07.957132502Z","aliases":["CVE-2026-115392","GHSA-vqjp-4c8c-hfgg"],"related":["RUSTSEC-2026-0482"],"summary":"Filesystem sandbox escape when paths or symlinks contain trailing slashes","details":"This is an entry in the RustSec database for the Wasmtime security advisory\nlocated at\nhttps://github.com/bytecodealliance/wasmtime/security/advisories/GHSA-vqjp-4c8c-hfgg\nFor more information see the GitHub-hosted security advisory.","affected":[{"package":{"name":"wasmtime","ecosystem":"crates.io","purl":"pkg:cargo/wasmtime"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"24.0.13"},{"introduced":"25.0.0"},{"fixed":"36.0.14"},{"introduced":"37.0.0"},{"fixed":"46.0.3"},{"introduced":"47.0.0"},{"fixed":"47.0.4"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":[],"cvss":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0269.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/wasmtime"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0269.html"},{"type":"WEB","url":"https://github.com/bytecodealliance/wasmtime/pull/14182"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:P/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0294.json b/data/crates.io/RUSTSEC-2026-0294.json index 556cda36ca..2bbe547842 100644 --- a/data/crates.io/RUSTSEC-2026-0294.json +++ b/data/crates.io/RUSTSEC-2026-0294.json @@ -1 +1 @@ -{"schema_version":"1.9.0","id":"RUSTSEC-2026-0294","published":"2026-09-18T12:00:00Z","modified":"2026-09-23T03:55:40.049477764Z","aliases":["CVE-2026-92612","GHSA-8mq4-3mwq-qvg6"],"summary":"Unsoundness in UTF-8 'String' trait","details":"The String API in iceoryx2-bb-container exposes its contents as mutable bytes through safe APIs, while String::as_str() converts those bytes into a Rust string slice without validating UTF-8. \nAn application can therefore create an invalid &str and trigger undefined behavior using entirely safe Rust.\n\nThe API methods `as_mut_bytes` and `deref_mut` where removed from the String API and their implementations (PolymorphicString, RelocatableString, and StaticString ) without replacement.\nLink to Pull-Request: https://github.com/eclipse-iceoryx/iceoryx2/pull/1956","affected":[{"package":{"name":"iceoryx2-bb-container","ecosystem":"crates.io","purl":"pkg:cargo/iceoryx2-bb-container"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"0.10.0"},{"introduced":"0.11.0-0"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["iceoryx2_bb_container::string::String::as_mut_bytes","iceoryx2_bb_container::string::String::deref_mut","iceoryx2_bb_container::string::polymorphic_string::PolymorphicString::as_mut_bytes","iceoryx2_bb_container::string::polymorphic_string::PolymorphicString::deref_mut","iceoryx2_bb_container::string::relocatable_string::RelocatableString::as_mut_bytes","iceoryx2_bb_container::string::relocatable_string::RelocatableString::deref_mut","iceoryx2_bb_container::string::static_string::StaticString::as_mut_bytes","iceoryx2_bb_container::string::static_string::StaticString::deref_mut"],"os":[]}},"database_specific":{"categories":[],"cvss":null,"informational":"unsound","source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0294.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/iceoryx2-bb-container"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0294.html"},{"type":"WEB","url":"https://github.com/eclipse-iceoryx/iceoryx2/pull/1956"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0294","published":"2026-09-18T12:00:00Z","modified":"2026-11-11T10:45:02.509154109Z","aliases":["CVE-2026-92612","GHSA-8mq4-3mwq-qvg6"],"summary":"Unsoundness in UTF-8 'String' trait","details":"The String API in iceoryx2-bb-container exposes its contents as mutable bytes through safe APIs, while String::as_str() converts those bytes into a Rust string slice without validating UTF-8. \nAn application can therefore create an invalid &str and trigger undefined behavior using entirely safe Rust.\n\nThe API methods `as_mut_bytes` and `deref_mut` where removed from the String API and their implementations (PolymorphicString, RelocatableString, and StaticString ) without replacement.\nLink to Pull-Request: https://github.com/eclipse-iceoryx/iceoryx2/pull/1956","affected":[{"package":{"name":"iceoryx2-bb-container","ecosystem":"crates.io","purl":"pkg:cargo/iceoryx2-bb-container"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"0.10.0"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["iceoryx2_bb_container::string::String::as_mut_bytes","iceoryx2_bb_container::string::String::deref_mut","iceoryx2_bb_container::string::polymorphic_string::PolymorphicString::as_mut_bytes","iceoryx2_bb_container::string::polymorphic_string::PolymorphicString::deref_mut","iceoryx2_bb_container::string::relocatable_string::RelocatableString::as_mut_bytes","iceoryx2_bb_container::string::relocatable_string::RelocatableString::deref_mut","iceoryx2_bb_container::string::static_string::StaticString::as_mut_bytes","iceoryx2_bb_container::string::static_string::StaticString::deref_mut"],"os":[]}},"database_specific":{"categories":[],"cvss":null,"informational":"unsound","source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0294.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/iceoryx2-bb-container"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0294.html"},{"type":"WEB","url":"https://github.com/eclipse-iceoryx/iceoryx2/pull/1956"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0480.json b/data/crates.io/RUSTSEC-2026-0480.json new file mode 100644 index 0000000000..021da5ba82 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0480.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0480","published":"2026-11-10T12:00:00Z","modified":"2026-11-11T08:45:02.464645086Z","aliases":["CVE-2026-40693","GHSA-gwf2-8595-qw97"],"summary":"Panic on a GCP launch endorsement without TDX measurements","details":"`GcpTdxHost::verify_launch_endorsement()`, available with the `host-gcp-tdx`\nfeature, read `uefi_golden.tdx.measurements[0]` without checking that the\ngolden measurement contains a TDX measurement. If a launch endorsement's\ngolden measurement had no TDX measurements, the call panicked with an index\nout of bounds instead of returning an error.\n\nThe panic is reached only after the endorsement's signing certificate and\nsignature have been verified. Triggering it therefore requires a correctly\nsigned endorsement that lacks TDX measurements.\n\nIntel describes the issue as improper input validation and classifies it as\ndenial of service, with a CVSS 4.0 base score of 5.7, in INTEL-SA-01520.\n\nThe flaw was corrected in\n[#87](https://github.com/IntelLabs/tdx-workload-attestation/pull/87), released\nin 0.2.0. The function now returns `Error::ParseError` when the TDX\nmeasurement structure is missing or empty.\n\nReported by [Borys Tsyrulnikov](https://borystsyrulnikov.dev).","affected":[{"package":{"name":"tdx_workload_attestation","ecosystem":"crates.io","purl":"pkg:cargo/tdx_workload_attestation"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"0.2.0"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["tdx_workload_attestation::gcp::GcpTdxHost::verify_launch_endorsement"],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":"CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0480.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/tdx_workload_attestation"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0480.html"},{"type":"WEB","url":"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01520.html"},{"type":"WEB","url":"https://github.com/IntelLabs/tdx-workload-attestation/pull/87"},{"type":"WEB","url":"https://github.com/IntelLabs/tdx-workload-attestation/releases/tag/v0.2.0"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:H/AT:P/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0481.json b/data/crates.io/RUSTSEC-2026-0481.json new file mode 100644 index 0000000000..f398269d26 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0481.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0481","published":"2026-11-10T12:00:00Z","modified":"2026-11-11T08:45:02.484411857Z","aliases":["CVE-2026-42067","GHSA-w4mf-755j-wjwc"],"summary":"`verify_x509_cert` does not check the certificate validity period","details":"`tdx_workload_attestation::verification::x509::verify_x509_cert()`, available\nwith the `host-verification` feature, checked that `issuer_cert` issued `cert`\nand verified the certificate signature. It did not check the certificate's\nvalidity period, so it accepted expired and not-yet-valid certificates.\n\nWith the `host-gcp-tdx` feature, this function validates the GCP launch\nendorsement signing certificate against the GCE confidential-computing TCB\nroot certificate. A signing certificate outside its validity period was\ntherefore still accepted during launch endorsement verification.\n\nIntel describes the issue as improper certificate validation and classifies\nit as escalation of privilege, with a CVSS 4.0 base score of 5.9, in\nINTEL-SA-01520.\n\nThe flaw was corrected in\n[#86](https://github.com/IntelLabs/tdx-workload-attestation/pull/86), released\nin 0.2.0. The function now returns `Ok(false)` when the current time is\noutside the certificate's `notBefore`/`notAfter` range.\n\nReported by [Borys Tsyrulnikov](https://borystsyrulnikov.dev).","affected":[{"package":{"name":"tdx_workload_attestation","ecosystem":"crates.io","purl":"pkg:cargo/tdx_workload_attestation"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"0.2.0"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["tdx_workload_attestation::verification::x509::verify_x509_cert"],"os":[]}},"database_specific":{"categories":["crypto-failure"],"cvss":"CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0481.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/tdx_workload_attestation"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0481.html"},{"type":"WEB","url":"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01520.html"},{"type":"WEB","url":"https://github.com/IntelLabs/tdx-workload-attestation/pull/86"},{"type":"WEB","url":"https://github.com/IntelLabs/tdx-workload-attestation/releases/tag/v0.2.0"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0482.json b/data/crates.io/RUSTSEC-2026-0482.json new file mode 100644 index 0000000000..c9d0b3b5af --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0482.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0482","published":"2026-08-20T12:00:00Z","modified":"2026-11-11T08:45:02.459487612Z","aliases":["GHSA-hp8f-xmx4-4qrg"],"related":["RUSTSEC-2026-0269"],"summary":"Paths or symlinks ending in a slash escape the sandbox","details":"cap-primitives resolves a path inside a sandboxed directory one component at a time and opens the last component with `O_NOFOLLOW`, so that a symlink there cannot lead out of the directory. When the path ended in `/`, the resolver added the slash back to the last component before opening it. With a trailing slash the operating system no longer treats that component as the last one, so `O_NOFOLLOW` does not apply, the symlink is followed, and the resulting file descriptor was returned without further checks.\n\nA symlink inside the sandbox that points outside it, such as `escape -> ../outside` or a link to `/`, can therefore be opened as `escape/`. If the symlink's own target ends in `/`, the caller's path does not need a trailing slash at all, so removing trailing slashes from untrusted input is not a workaround. The maintainers' advisory reports reads and writes outside the sandbox through `open`, `open_dir`, `read_dir` and `canonicalize`, and directory creation, removal and renames outside it through `create_dir`, `remove_dir` and `rename`.\n\nThis resolver is used on Windows and macOS, on FreeBSD when `O_RESOLVE_BENEATH` is not available, and on Linux when `openat2` is not available (kernels before 5.6, or a seccomp policy that rejects it). cap-std and other crates built on cap-primitives are affected through it. Wasmtime's WASI filesystem support was affected as well; that is covered by RUSTSEC-2026-0269.\n\nThe fix stops re-adding the trailing slash and is in 3.4.6 and 4.0.3.","affected":[{"package":{"name":"cap-primitives","ecosystem":"crates.io","purl":"pkg:cargo/cap-primitives"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"},{"fixed":"3.4.6"},{"introduced":"4.0.0"},{"fixed":"4.0.3"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["file-disclosure"],"cvss":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0482.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/cap-primitives"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0482.html"},{"type":"WEB","url":"https://github.com/bytecodealliance/cap-std/pull/419"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:N"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0483.json b/data/crates.io/RUSTSEC-2026-0483.json new file mode 100644 index 0000000000..ee9036b2fa --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0483.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0483","published":"2026-11-10T12:00:00Z","modified":"2026-11-11T08:45:02.467620273Z","summary":"`lwd-uri-util` was removed from crates.io for malicious code","details":"The `lwd-uri-util` crate exfiltrated SSH keys, AWS credentials, and other\nenvironmental metadata and secrets when any crate using it was built.\n\nThe malicious crate had 8 versions published on 2026-11-09 approximately 12\nhours before removal and had no evidence of actual usage. This crate had no\ndependencies on crates.io.\n\nThanks to [Bitbison](https://bitbison.io/) for the report.","affected":[{"package":{"name":"lwd-uri-util","ecosystem":"crates.io","purl":"pkg:cargo/lwd-uri-util"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.0.0-0"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["malicious"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0483.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/lwd-uri-util"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0483.html"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0484.json b/data/crates.io/RUSTSEC-2026-0484.json new file mode 100644 index 0000000000..9559bc7d8f --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0484.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0484","published":"2026-08-11T12:00:00Z","modified":"2026-11-11T08:45:02.464648743Z","aliases":["CVE-2026-20765","GHSA-56j3-r4r2-x3x6"],"summary":"Inverted bitmap validation in `invalidate_l2_cached_ept`","details":"`tdx_guest::tdcall::invalidate_l2_cached_ept()` takes a bitmap that selects the\nL2 VMs whose cached EPT translations should be invalidated. Bits 1 through 3\nselect L2 VMs 1 through 3, and all other bits are reserved. The input check\nwas inverted:\n\n if l2_vm_idx_bitmap & !0b1110 == 0 {\n return Err(TdCallError::TdxOperandInvalid);\n }\n\n`l2_vm_idx_bitmap & !0b1110` isolates the bits that are not allowed. A result\nof zero means the bitmap is valid, and the function returned an error in that\ncase. Every valid bitmap, such as `0b0010` or `0b1110`, was therefore rejected\nwith `TdxOperandInvalid`. Bitmaps with reserved bits set, such as `0b0001` or\n`0b1_0000`, were forwarded to the TDX module.\n\nIntel describes the issue as an incorrect comparison and classifies it as\nescalation of privilege, with a CVSS 4.0 base score of 4.6, in\nINTEL-SA-01462.\n\nThe flaw was corrected in commit\n[`311d33b`](https://github.com/intel/confidential-computing.tdx.tdx-guest/commit/311d33b18a5bef5dc67c9b0c86587dd00c77027e),\nreleased in 0.3.1. The function now rejects an empty bitmap and any bit\noutside `0b1110`.\n\nReported by [Borys Tsyrulnikov](https://borystsyrulnikov.dev).","affected":[{"package":{"name":"tdx-guest","ecosystem":"crates.io","purl":"pkg:cargo/tdx-guest"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.1.8"},{"fixed":"0.3.1"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["tdx_guest::tdcall::invalidate_l2_cached_ept"],"os":[]}},"database_specific":{"categories":["privilege-escalation"],"cvss":"CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0484.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/tdx-guest"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0484.html"},{"type":"WEB","url":"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01462.html"},{"type":"WEB","url":"https://github.com/intel/confidential-computing.tdx.tdx-guest/commit/311d33b18a5bef5dc67c9b0c86587dd00c77027e"},{"type":"WEB","url":"https://borystsyrulnikov.dev/posts/tdx-guest-l2-vm-bugs/"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0485.json b/data/crates.io/RUSTSEC-2026-0485.json new file mode 100644 index 0000000000..29ef9b35f5 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0485.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0485","published":"2026-08-11T12:00:00Z","modified":"2026-11-11T08:45:02.456713599Z","aliases":["CVE-2026-20763","GHSA-wgvc-hf5h-m5h7"],"summary":"`enter_l2_vcpu` passes the wrong RCX operand to `TDG.VP.ENTER`","details":"`tdx_guest::tdcall::enter_l2_vcpu()` invokes the `TDG.VP.ENTER` TDCALL. RCX\nmust carry the L2 VM index in bits 53:52 and the invalidation control value\n(`InvdTranslations`, 0 to 3) in bits 1:0. The function computed that value as\n`(l2_vm_idx << 52) | (invd_translations as u64)`, but then passed the raw\n`l2_vm_idx` in RCX instead.\n\nAs a result, the TDX module received an operand that differs from the\ncaller's request. The VM index field was 0, and the low bits of the index\nwere interpreted as the invalidation control value. For example, VM 1 with\n`InvdTlb` (2) should produce `0x0010_0000_0000_0002`, but produced `0x1`.\n\nIntel describes the issue as an incorrect calculation and classifies it as\nescalation of privilege, with a CVSS 4.0 base score of 4.6, in\nINTEL-SA-01462.\n\nThe flaw was corrected in commit\n[`311d33b`](https://github.com/intel/confidential-computing.tdx.tdx-guest/commit/311d33b18a5bef5dc67c9b0c86587dd00c77027e),\nreleased in 0.3.1, which passes the computed RCX value.\n\nReported by [Borys Tsyrulnikov](https://borystsyrulnikov.dev).","affected":[{"package":{"name":"tdx-guest","ecosystem":"crates.io","purl":"pkg:cargo/tdx-guest"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.1.8"},{"fixed":"0.3.1"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["tdx_guest::tdcall::enter_l2_vcpu"],"os":[]}},"database_specific":{"categories":["privilege-escalation"],"cvss":"CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N","informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0485.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/tdx-guest"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0485.html"},{"type":"WEB","url":"https://www.intel.com/content/www/us/en/security-center/advisory/intel-sa-01462.html"},{"type":"WEB","url":"https://github.com/intel/confidential-computing.tdx.tdx-guest/commit/311d33b18a5bef5dc67c9b0c86587dd00c77027e"},{"type":"WEB","url":"https://borystsyrulnikov.dev/posts/tdx-guest-l2-vm-bugs/"}],"database_specific":{"license":"CC0-1.0"},"severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:L/AC:L/AT:N/PR:H/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N"}]} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0486.json b/data/crates.io/RUSTSEC-2026-0486.json new file mode 100644 index 0000000000..42b25c8111 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0486.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0486","published":"2026-10-27T12:00:00Z","modified":"2026-11-11T08:45:02.466292839Z","aliases":["GHSA-jh2m-fhpj-3vr7"],"summary":"xattr functions mark unwritten bytes as initialized when given a zero-length buffer","details":"The extended-attribute functions in `rustix::fs` (`getxattr`, `lgetxattr`, `fgetxattr`, `listxattr`, `llistxattr` and `flistxattr`) write into a `Buffer` and then mark as many bytes initialized as the system call reported. When the buffer has zero length, the operating system treats the call as a size query: it writes nothing and returns the size of the attribute value or of the name list. rustix still marked that many bytes as initialized.\n\nPassing a `Vec` whose length equals its capacity through `spare_capacity` gives such a zero-length buffer, and rustix then set the vector's length past its capacity over memory that was never written. This is undefined behavior reachable from safe code. With an empty `&mut [MaybeUninit]` the call panics instead.\n\nFrom 1.1.6 these functions return an empty result for a zero-length buffer, and new `getxattr_query`, `lgetxattr_query`, `fgetxattr_query`, `listxattr_query`, `llistxattr_query` and `flistxattr_query` functions return the size. The 0.38 series did not have the `Buffer` API and is not affected.","affected":[{"package":{"name":"rustix","ecosystem":"crates.io","purl":"pkg:cargo/rustix"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.0.0"},{"fixed":"1.1.6"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":["rustix::fs::fgetxattr","rustix::fs::flistxattr","rustix::fs::getxattr","rustix::fs::lgetxattr","rustix::fs::listxattr","rustix::fs::llistxattr"],"os":[]}},"database_specific":{"categories":[],"cvss":null,"informational":"unsound","source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0486.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/rustix"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0486.html"},{"type":"WEB","url":"https://github.com/bytecodealliance/rustix/commit/8e6aaa6713d2bb64b1fdd8c472e3a32a10a76d70"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0487.json b/data/crates.io/RUSTSEC-2026-0487.json new file mode 100644 index 0000000000..4f9f7fa1a7 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0487.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0487","published":"2026-09-03T12:00:00Z","modified":"2026-11-11T12:30:02.446157507Z","aliases":["GHSA-hx8c-fjhc-hmf5"],"summary":"Panic on a malformed `alpn` value when parsing SVCB or HTTPS records from text","details":"When an SVCB or HTTPS record is parsed from its text (zone file) form, the `alpn` service parameter value is split into a list of protocol names. That step called `expect` on the result, on the assumption that it could not fail. A malformed value, such as one with an unbalanced quote (`alpn=\"h`), two commas in a row or an invalid escape, makes it fail, so the parser panicked instead of returning an error.\n\nCode that parses zone files or record text from an untrusted source can be made to panic, which is a denial of service. Records decoded from the DNS wire format are not parsed this way.\n\nThe fix returns a parse error instead. It is in 0.26.2.","affected":[{"package":{"name":"hickory-proto","ecosystem":"crates.io","purl":"pkg:cargo/hickory-proto"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.20.1"},{"fixed":"0.26.2"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0487.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/hickory-proto"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0487.html"},{"type":"WEB","url":"https://github.com/hickory-dns/hickory-dns/commit/80eaf9e79f7561e2bc1cf5b89eaba7fd25de3284"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0488.json b/data/crates.io/RUSTSEC-2026-0488.json new file mode 100644 index 0000000000..060ab25479 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0488.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0488","published":"2026-09-03T12:00:00Z","modified":"2026-11-11T12:30:02.896196450Z","aliases":["GHSA-x962-5xwx-fr8x"],"summary":"Integer underflow when reporting a malformed TSIG record","details":"When a TSIG record is decoded from the DNS wire format, its MAC length and \"other data\" length are checked against the end of the record data. If a check failed, the error value was built from `end_idx - decoder.index()`. For a malformed record the decoder can already be past `end_idx`, so this unsigned subtraction underflows.\n\nIn builds with overflow checks enabled (the `dev` profile by default) the underflow panics, so a peer that sends a crafted TSIG record can crash the process. In builds without overflow checks the result wraps and is only used in the returned error.\n\nThe fix returns a plain \"insufficient bytes\" error without the subtraction. It is in 0.26.2.","affected":[{"package":{"name":"hickory-proto","ecosystem":"crates.io","purl":"pkg:cargo/hickory-proto"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.26.0"},{"fixed":"0.26.2"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0488.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/hickory-proto"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0488.html"},{"type":"WEB","url":"https://github.com/hickory-dns/hickory-dns/commit/b1246a8a21b6dcc34d427a7d39dd96e10825c384"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0489.json b/data/crates.io/RUSTSEC-2026-0489.json new file mode 100644 index 0000000000..7f6004ef26 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0489.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0489","published":"2026-09-03T12:00:00Z","modified":"2026-11-11T12:30:02.444925481Z","aliases":["GHSA-wgfr-mphw-j5g4"],"summary":"Panic when an SVCB or HTTPS parameter value is a single quote character","details":"When an SVCB or HTTPS record is parsed from its text (zone file) form, surrounding double quotes are removed from each service parameter value by checking that the value starts and ends with `\"` and slicing off the first and last character. A value that is a single `\"` (for example `alpn=\"`) passes that check, and the slice `[1..0]` panics.\n\nCode that parses zone files or record text from an untrusted source can be made to panic, which is a denial of service. Records decoded from the DNS wire format are not parsed this way.\n\nThe fix rejects a lone quote character with a parse error before the slice. It is in 0.26.2.","affected":[{"package":{"name":"hickory-proto","ecosystem":"crates.io","purl":"pkg:cargo/hickory-proto"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.20.1"},{"fixed":"0.26.2"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0489.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/hickory-proto"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0489.html"},{"type":"WEB","url":"https://github.com/hickory-dns/hickory-dns/commit/c663b60a4b4b98deb45d16ccbf10ce2f76cd88a9"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0490.json b/data/crates.io/RUSTSEC-2026-0490.json new file mode 100644 index 0000000000..0dbfd6b115 --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0490.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0490","published":"2026-09-03T12:00:00Z","modified":"2026-11-11T12:41:09.446683259Z","aliases":["CVE-2026-123574","GHSA-4rph-pmrw-mwpw"],"summary":"Zone file lexer panics on a token of 4096 or more characters","details":"The zone file lexer read each token in a loop capped at 4,096 iterations, with an `assert!` on the loop counter as a safety bound. A single token long enough to reach that bound, for example a very long name or character string in a zone file, triggered the assertion and the parser panicked instead of returning the token or an error.\n\nCode that parses zone files from an untrusted source can be made to panic, which is a denial of service.\n\nThe fix removes the iteration bound, so the lexer ends only when it reaches the end of a token or an error. It is in 0.26.2.","affected":[{"package":{"name":"hickory-proto","ecosystem":"crates.io","purl":"pkg:cargo/hickory-proto"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.24.0"},{"fixed":"0.26.2"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0490.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/hickory-proto"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0490.html"},{"type":"WEB","url":"https://github.com/hickory-dns/hickory-dns/commit/0494e15c88fba612f9ecb4f013eb3b221822c867"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/crates.io/RUSTSEC-2026-0491.json b/data/crates.io/RUSTSEC-2026-0491.json new file mode 100644 index 0000000000..c0ca48cb7b --- /dev/null +++ b/data/crates.io/RUSTSEC-2026-0491.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"RUSTSEC-2026-0491","published":"2026-09-03T12:00:00Z","modified":"2026-11-11T12:30:02.901694858Z","aliases":["GHSA-67wc-6jq8-ghrc"],"summary":"Memory amplification from record counts in the DNS message header","details":"When a DNS message is decoded, the vectors for the question, answer, authority and additional sections were created with `Vec::with_capacity` set to the counts in the message header. Those counts come from the sender and are not checked against the size of the message, so a single small packet that claims 65,535 records makes the decoder reserve memory for that many records (about 18 MB, according to the upstream advisory) before it reads any record and fails.\n\nAny code that decodes untrusted DNS messages is exposed. That includes servers, and clients and resolvers that receive spoofed responses. Repeated packets can drive memory use up and cause a denial of service.\n\nThe fix caps the initial capacity at 8 entries and lets the vectors grow as records are actually decoded. It is in 0.26.2.","affected":[{"package":{"name":"hickory-proto","ecosystem":"crates.io","purl":"pkg:cargo/hickory-proto"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0.18.0"},{"fixed":"0.26.2"}]}],"ecosystem_specific":{"affected_functions":null,"affects":{"arch":[],"functions":[],"os":[]}},"database_specific":{"categories":["denial-of-service"],"cvss":null,"informational":null,"source":"https://github.com/rustsec/advisory-db/blob/osv/crates/RUSTSEC-2026-0491.json"}}],"references":[{"type":"PACKAGE","url":"https://crates.io/crates/hickory-proto"},{"type":"ADVISORY","url":"https://rustsec.org/advisories/RUSTSEC-2026-0491.html"},{"type":"WEB","url":"https://github.com/hickory-dns/hickory-dns/commit/280c0e5c4e3002d73ba0762835ef82272d1d33a6"}],"database_specific":{"license":"CC0-1.0"}} \ No newline at end of file diff --git a/data/npm/MAL-2026-18794.json b/data/npm/MAL-2026-18794.json index deef82b11c..da1a7b5306 100644 --- a/data/npm/MAL-2026-18794.json +++ b/data/npm/MAL-2026-18794.json @@ -1 +1 @@ -{"schema_version":"1.9.2","id":"MAL-2026-18794","published":"2026-11-09T16:53:31Z","modified":"2026-11-09T17:30:04.034135352Z","aliases":["GHSA-p9gj-xrcq-w5rc"],"summary":"Malicious code in dsh-ungate (npm)","details":"\n---\n_-= Per source details. Do not edit below this line.=-_\n\n## Source: ghsa-malware (a660f4353216e548bc40977463bcd181435354a0aed48c8c6f5166f684061176)\nAny computer that has this package installed or running should be considered fully compromised. All secrets and keys stored on that computer should be rotated immediately from a different computer. The package should be removed, but as full control of the computer may have been given to an outside entity, there is no guarantee that removing the package will remove all malicious software resulting from installing it.\n","affected":[{"package":{"name":"dsh-ungate","ecosystem":"npm","purl":"pkg:npm/dsh-ungate"},"versions":["0.2.3"],"database_specific":{"cwes":[{"cweId":"CWE-506","description":"The product contains code that appears to be malicious in nature.","name":"Embedded Malicious Code"}],"source":"https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/dsh-ungate/MAL-2026-18794.json"}}],"references":[{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-p9gj-xrcq-w5rc"}],"database_specific":{"malicious-packages-origins":[{"id":"GHSA-p9gj-xrcq-w5rc","import_time":"2026-11-09T17:18:22.621460806Z","modified_time":"2026-11-09T16:54:01Z","sha256":"a660f4353216e548bc40977463bcd181435354a0aed48c8c6f5166f684061176","source":"ghsa-malware","versions":["0.2.3"]}]}} \ No newline at end of file +{"schema_version":"1.9.2","id":"MAL-2026-18794","published":"2026-11-09T16:53:31Z","modified":"2026-11-11T07:30:07.877574058Z","aliases":["GHSA-p9gj-xrcq-w5rc"],"summary":"Malicious code in dsh-ungate (npm)","details":"\n---\n_-= Per source details. Do not edit below this line.=-_\n\n## Source: ghsa-malware (a660f4353216e548bc40977463bcd181435354a0aed48c8c6f5166f684061176)\nAny computer that has this package installed or running should be considered fully compromised. All secrets and keys stored on that computer should be rotated immediately from a different computer. The package should be removed, but as full control of the computer may have been given to an outside entity, there is no guarantee that removing the package will remove all malicious software resulting from installing it.\n","affected":[{"package":{"name":"dsh-ungate","ecosystem":"npm","purl":"pkg:npm/dsh-ungate"},"versions":["0.2.3","0.2.4"],"database_specific":{"cwes":[{"cweId":"CWE-506","description":"The product contains code that appears to be malicious in nature.","name":"Embedded Malicious Code"},{"cweId":"CWE-506","description":"The product contains code that appears to be malicious in nature.","name":"Embedded Malicious Code"}],"source":"https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/dsh-ungate/MAL-2026-18794.json"}}],"references":[{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-p9gj-xrcq-w5rc"}],"database_specific":{"malicious-packages-origins":[{"id":"GHSA-p9gj-xrcq-w5rc","import_time":"2026-11-09T17:18:22.621460806Z","modified_time":"2026-11-09T16:54:01Z","sha256":"a660f4353216e548bc40977463bcd181435354a0aed48c8c6f5166f684061176","source":"ghsa-malware","versions":["0.2.3"]},{"id":"GHSA-p9gj-xrcq-w5rc","import_time":"2026-11-11T07:18:44.970344723Z","modified_time":"2026-11-11T06:45:10Z","sha256":"38bfa4e95a344ec7c217f8aee7bf88163fc24d2983f1804c612cebb625b457b5","source":"ghsa-malware","versions":["0.2.4","0.2.3"]}]}} \ No newline at end of file diff --git a/data/npm/MAL-2026-19118.json b/data/npm/MAL-2026-19118.json new file mode 100644 index 0000000000..652278ef17 --- /dev/null +++ b/data/npm/MAL-2026-19118.json @@ -0,0 +1 @@ +{"schema_version":"1.9.2","id":"MAL-2026-19118","published":"2026-11-11T12:09:02Z","modified":"2026-11-11T12:30:04.004300065Z","aliases":["GHSA-9j97-6pqr-qvrx"],"summary":"Malicious code in 0sec-cli (npm)","details":"\n---\n_-= Per source details. Do not edit below this line.=-_\n\n## Source: ghsa-malware (6ca0694213fab27d0292d43aee954b81c13e551a81563903fb9afd7e54a9dda2)\nAny computer that has this package installed or running should be considered fully compromised. All secrets and keys stored on that computer should be rotated immediately from a different computer. The package should be removed, but as full control of the computer may have been given to an outside entity, there is no guarantee that removing the package will remove all malicious software resulting from installing it.\n","affected":[{"package":{"name":"0sec-cli","ecosystem":"npm","purl":"pkg:npm/0sec-cli"},"versions":["0.28.10"],"database_specific":{"cwes":[{"cweId":"CWE-506","description":"The product contains code that appears to be malicious in nature.","name":"Embedded Malicious Code"}],"source":"https://github.com/ossf/malicious-packages/blob/main/osv/malicious/npm/0sec-cli/MAL-2026-19118.json"}}],"references":[{"type":"ADVISORY","url":"https://github.com/advisories/GHSA-9j97-6pqr-qvrx"}],"database_specific":{"malicious-packages-origins":[{"id":"GHSA-9j97-6pqr-qvrx","import_time":"2026-11-11T12:23:59.195275939Z","modified_time":"2026-11-11T12:09:02Z","sha256":"6ca0694213fab27d0292d43aee954b81c13e551a81563903fb9afd7e54a9dda2","source":"ghsa-malware","versions":["0.28.10"]}]}} \ No newline at end of file diff --git a/state/sync.json b/state/sync.json index 41cf6934b3..f1047b149d 100644 --- a/state/sync.json +++ b/state/sync.json @@ -37,10 +37,10 @@ "records": 7422 }, "PyPI": { - "etag": "\"173a45dd51b7d52ca52da6fc4170b3fe\"", - "last_modified": "Wed, 11 Nov 2026 03:02:53 GMT", - "last_success": "2026-11-11T06:41:36.296745+00:00", - "records": 26860 + "etag": "\"75548fd5b36823e8b36196f008408c1a\"", + "last_modified": "Wed, 11 Nov 2026 10:33:08 GMT", + "last_success": "2026-11-11T12:42:28.640598+00:00", + "records": 26862 }, "RubyGems": { "etag": "\"b38b3597289377dfc31f237d94a35f67\"", @@ -49,16 +49,16 @@ "records": 5501 }, "crates.io": { - "etag": "\"e293a0d346823739ae4e973548efe465\"", - "last_modified": "Tue, 10 Nov 2026 20:03:04 GMT", - "last_success": "2026-11-11T00:43:57.991487+00:00", - "records": 3081 + "etag": "\"5034d460b44c481025af03f84cfae50c\"", + "last_modified": "Wed, 11 Nov 2026 12:47:47 GMT", + "last_success": "2026-11-11T12:50:41.689284+00:00", + "records": 3093 }, "npm": { - "etag": "\"fb52c38d1f574b47ead3e79c1b3a5594\"", - "last_modified": "Wed, 11 Nov 2026 04:32:56 GMT", - "last_success": "2026-11-11T06:43:33.882735+00:00", - "records": 231665 + "etag": "\"ae5e057b4685c9a089e5afd2e3a83f19\"", + "last_modified": "Wed, 11 Nov 2026 12:32:53 GMT", + "last_success": "2026-11-11T12:50:33.290104+00:00", + "records": 231666 } }, "version": 1