← WebKit Silent-Fix Report — 2026-W25

b9c7a1bf56  Prevent untrusted image data from reaching ImageIO codec dispatch via WebExtension icon loading

severity medium class Other confidence 0.70 WebKit WebExtensions image loading exploitable-grade
Anthony Tarbinian Thu Jun 18 02:55:29 2026 -0700 full: b9c7a1bf56857f208d51df25317dbbf9501cdd21 bug report ↗ view on GitHub ↗
Primitive: untrusted bytes reaching ImageIO codecs
Triage note: Attack-surface reduction limiting untrusted extension icon data to the vetted image-type set before codec dispatch.
Contents

The bug at a glance

This is an attack-surface reduction fix: an attacker-supplied WebExtension icon could steer which ImageIO codec runs in the UIProcess by controlling magic bytes, exposing exotic, less-scrutinized decoders (PSD, OpenEXR, TIFF) to untrusted data in a privileged process. No specific memory-corruption CVE is claimed in the patch, and the fix hardens rather than repairs a single overflow, so medium severity reflects a plausible but not demonstrated path to UIProcess compromise via a decoder bug.

The old code funneled untrusted extension icon bytes straight into [NSImage/UIImage initWithData:], which sniffs magic bytes and dispatches to whichever ImageIO codec matches, including exotic formats. The fix inserts a type gate: extract the type with CGImageSourceGetType, allow it only if WebCore::isSupportedImageType accepts it, and otherwise refuse, so untrusted icons can only reach the same vetted decoders used for web content.

Root cause

WebExtension::iconForPath decodes an icon resource from an extension bundle into a CocoaImage for display in browser UI. This runs in the UIProcess. Previously the terminal fallback was result = [[CocoaImage alloc] initWithData:imageData] (and on iOS initWithData:scale:). NSImage/UIImage initWithData: inspects the leading bytes of the data and hands them to ImageIO, which supports a large set of formats far beyond the web-safe set, PSD, OpenEXR, TIFF, and others. An extension author fully controls the icon bytes, so by writing the magic header of an exotic format they choose which decoder ImageIO invokes. Many of these exotic codecs receive far less fuzzing and hardening than PNG/JPEG/GIF/WebP, so this is an attacker-selectable route to a large, weakly-audited parsing surface running in a privileged process.

The patch introduces WebKit::createCocoaImageRestrictedToSupportedTypes in CocoaImage.mm. It builds a CGImageSource from the data (CGImageSourceCreateWithData), reads the concrete type with CGImageSourceGetType, and rejects anything for which WebCore::isSupportedImageType(type) is false, returning nil. Only if the type is on the web-supported allow-list does it decode via CGImageSourceCreateImageAtIndex and wrap the CGImage in an NSImage/UIImage. Because the decision is made from ImageIO’s own detected type rather than from a filename or MIME string, an attacker cannot rename a PSD to .png (or point a .svg path at PSD bytes) to slip past.

SVG is a deliberate exception: it is a vector format that does not flow through CGImageSource until rasterized, so the old code had a dedicated SVG branch. Removing the blanket initWithData: call would have broken SVG icons on macOS, which previously relied on NSImage initWithData:. The patch reroutes macOS SVG through [_NSSVGImageRep initWithData:] (guarded by NSClassFromString), mirroring the existing iOS CGSVGDocumentCreateFromData path, and keeps the SVG branch ahead of the bitmap allow-list.

iconForPath is restructured so that after the SVG branch, if result is still nil it calls createCocoaImageRestrictedToSupportedTypes; if that also returns nil the function now returns makeUnexpected with a new InvalidIcon error (‘The image is not in a supported format.’) instead of silently falling back to the unrestricted initWithData:. This is the crux: the unrestricted decode path is deleted, not merely bypassed. The two added tests confirm both halves: a PSD (magic bytes ‘8’,‘B’,‘P’,‘S’) supplied as an icon, even when the icon path claims a .svg extension, is rejected with WKWebExtensionErrorInvalidManifestEntry, while a genuine SVG through the icons key still loads.

Key code

The new type-gated decoder: only web-supported ImageIO types are decoded

RetainPtr<CocoaImage> createCocoaImageRestrictedToSupportedTypes(NSData *data, double displayScale)
{
    if (!data.length)
        return nil;

    RetainPtr imageSource = adoptCF(CGImageSourceCreateWithData((__bridge CFDataRef)data, nullptr));
    if (!imageSource)
        return nil;

    RetainPtr type = CGImageSourceGetType(imageSource.get());
    if (!type || !WebCore::isSupportedImageType(type.get()))
        return nil;

    RetainPtr image = adoptCF(CGImageSourceCreateImageAtIndex(imageSource.get(), 0, nullptr));
    if (!image)
        return nil;

#if USE(APPKIT)
    UNUSED_PARAM(displayScale);
    return adoptNS([[NSImage alloc] initWithCGImage:image.get() size:NSZeroSize]);
#else
    return retainPtr([UIImage imageWithCGImage:image.get() scale:displayScale orientation:UIImageOrientationUp]);
#endif
}

Patch walkthrough

  • Source/WebKit/Platform/cocoa/CocoaImage.mm — Adds createCocoaImageRestrictedToSupportedTypes(NSData*, double displayScale): rejects empty data, creates a CGImageSource, reads CGImageSourceGetType, and returns nil unless WebCore::isSupportedImageType accepts the type; only then decodes via CGImageSourceCreateImageAtIndex and wraps in NSImage (APPKIT) or UIImage. This is the type-gated replacement for the arbitrary-codec initWithData: path.
  • Source/WebKit/Platform/cocoa/CocoaImage.h — Declares the new helper with a comment stating it restricts ImageIO to the web-content-supported type set and returns nil otherwise.
  • Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionCocoa.mm — Rewrites WebExtension::iconForPath: keeps a dedicated SVG branch (now routing macOS SVG through _NSSVGImageRep initWithData:, iOS through CGSVGDocumentCreateFromData), then for non-SVG calls createCocoaImageRestrictedToSupportedTypes, and if that returns nil returns an InvalidIcon error instead of the old unrestricted [[CocoaImage alloc] initWithData:]. Removes both unrestricted initWithData: fallbacks (macOS and iOS).
  • Source/WebCore/en.lproj/Localizable.strings — Adds the user-facing error string ‘Failed to load image for path “%@”. The image is not in a supported format.’ used by the new rejection path.
  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtension.mm — Adds IconsWithUnsupportedFormatsAreRejected (PSD bytes rejected, including when the path lies about being .svg) and SVGIconViaIconsKeyLoads (a real SVG still decodes), locking in both the restriction and the SVG exception.

Background

ImageIO codec dispatch by magic bytes — NSImage/UIImage initWithData: and CGImageSource infer the image type from the data’s leading bytes and route to the corresponding ImageIO decoder. Because the type is chosen from content, an attacker who controls the bytes controls which decoder executes, independent of any declared file extension or MIME type.

WebCore::isSupportedImageType — The allow-list predicate that WebKit uses to decide whether an image UTI is one of the formats it exposes to web content (the well-fuzzed set: PNG, JPEG, GIF, WebP, etc.). Reusing it here binds the extension-icon path to the same vetted surface rather than all of ImageIO’s exotic formats.

WebExtension icons in the UIProcess — Extension manifests declare icons (icons key, action/browser_action/page_action default_icon) whose bytes come from the extension bundle. iconForPath decodes them in the UIProcess for toolbar/badge display, so untrusted decoding here happens in a privileged process outside the WebContent sandbox.

Exotic decoders (PSD, OpenEXR, TIFF) — ImageIO supports many legacy/professional formats whose parsers are large and comparatively lightly hardened. Steering untrusted data into these decoders is a classic way to reach a memory-corruption bug; restricting to the web-supported set removes them from the reachable surface.

_NSSVGImageRep vs CGSVGDocument — SVG is vector and bypasses CGImageSource until rasterization. The patch preserves SVG support by routing macOS through the private _NSSVGImageRep initWithData: (looked up via NSClassFromString) and iOS through CGSVGDocumentCreateFromData, keeping SVG functional without reopening the arbitrary-bitmap-codec path.

Vulnerability window

  1. Exposure — iconForPath decodes attacker-controlled extension icon bytes via [NSImage/UIImage initWithData:], which dispatches to any ImageIO codec matching the magic bytes.
  2. Attacker control — An extension author sets the first bytes to an exotic format’s signature (e.g. PSD ‘8BPS’) to force a specific, weakly-audited decoder to run in the UIProcess.
  3. Fix design — createCocoaImageRestrictedToSupportedTypes gates decoding on CGImageSourceGetType + WebCore::isSupportedImageType; SVG handled by a dedicated branch.
  4. Hard failure — iconForPath now returns an InvalidIcon error instead of falling back to the unrestricted initWithData:, so unsupported types cannot decode at all.
  5. Regression lock — Added tests reject PSD bytes (even with a lying .svg path) and confirm a genuine SVG still loads via the icons key.

Proof of concept

Verbatim added test. It supplies Photoshop (PSD) bytes as a WebExtension icon and asserts iconForSize/actionIconForSize now return nil and an InvalidManifestEntry error is emitted. A second portion of the test (not shown) points a .svg icon path at the same PSD bytes to confirm the path-derived MIME type cannot bypass the type restriction. This demonstrates reachability of an exotic ImageIO decoder from an attacker-controlled icon, not a memory-corruption crash itself.

TEST(WKWebExtension, IconsWithUnsupportedFormatsAreRejected)
{
    static constexpr uint8_t photoshopBytes[] = {
        '8', 'B', 'P', 'S', 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00,
        0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00
    };

    auto *photoshopData = [NSData dataWithBytes:photoshopBytes length:sizeof(photoshopBytes)];
    auto *photoshopDataURL = [@"data:image/png;base64," stringByAppendingString:[photoshopData base64EncodedStringWithOptions:0]];

    auto *testManifestDictionary = @{
        @"manifest_version": @3,
        @"name": @"Test",
        @"version": @"1.0",
        @"description": @"Test",
        @"icons": @{ @"128": @"icon-128.psd" },
        @"action": @{ @"default_icon": @{ @"128": photoshopDataURL } }
    };

    auto *resources = @{ @"icon-128.psd": photoshopData };

    auto testExtension = [[WKWebExtension alloc] _initWithManifestDictionary:testManifestDictionary resources:resources];
    EXPECT_NULL([testExtension iconForSize:CGSizeMake(128, 128)]);
    EXPECT_NULL([testExtension actionIconForSize:CGSizeMake(128, 128)]);
    EXPECT_NOT_NULL(matchingError(testExtension.errors, WKWebExtensionErrorInvalidManifestEntry));

Exploitation

  1. Delivery — Attacker publishes or sideloads a WebExtension whose icon resource carries the magic bytes of an exotic format; iconForPath decodes it in the UIProcess during icon rendering.
  2. Codec selection — Choosing the format signature selects the ImageIO decoder that runs, aiming for one with a known or fuzzable parsing bug; this is the primitive the patch removes, not a completed exploit.
  3. Corruption (inferred) — Exploitation to code execution would depend on a separate vulnerability inside the selected exotic decoder; the patch itself demonstrates only that the exotic decoders were reachable, and provides no memory-corruption PoC. Treat any RCE claim as unproven attack-surface reduction.

Detection & hunting

For defenders and SOC / detection engineers:

  • ImageIO decoding of non-web types in UIProcess — Instrument or log CGImageSourceGetType results for extension icon decoding; any type outside WebCore::isSupportedImageType (PSD/OpenEXR/TIFF/etc.) reaching a decoder indicates the pre-patch behavior or a bypass.
  • UIProcess crashes in exotic ImageIO codecs — Crash reports with extension icon loading on the stack faulting inside PSD/EXR/TIFF/RAW ImageIO codecs are strong indicators of exploitation attempts against this surface.
  • Manifest/extension icon audits — Scan extension bundles for icon files whose magic bytes disagree with their declared extension/MIME (e.g. a .png or .svg that is actually PSD), which is the shape the added test exercises.

Audit directions

  • Other untrusted initWithData: sinks — Grep for [NSImage/UIImage initWithData:] and CGImageSourceCreateWithData across UIProcess/GPUProcess code paths that decode extension-, download-, or IPC-supplied bytes, and confirm each is gated by isSupportedImageType or equivalent.
  • MIME/extension-derived type trust — Audit any code that decides decode behavior from resourceMIMETypeForPath or filename extension rather than sniffed content; the test shows a lying .svg path must not influence the restriction.
  • SVG rasterization path — Review the new _NSSVGImageRep and CGSVGDocumentCreateFromData branches for their own untrusted-input handling, since SVG is deliberately exempt from the bitmap allow-list.
  • isSupportedImageType completeness — Verify the allow-list itself excludes formats whose decoders are weakly hardened, and that it is applied consistently wherever untrusted images enter privileged processes.

Before / after

Loading diff…