← All reports

[2] UIProcess decodes WebContent-supplied image bytes outside the web-safe allow-list

MediumWebKit UIProcessSandboxEscape

Renderer bytes reached the host app's full codec set — PSD, RAW, EXR.

b2dca99

Rated Medium: this patch adds no memory-safety fix, it restores a containment boundary. Three UIProcess paths handed renderer-supplied bytes to the platform's complete codec set, so any parser bug in PSD, OpenEXR, camera RAW, or JPEG 2000 became reachable from a compromised renderer inside the host application's own process.

Browser engines keep image decoding on a short leash: image parsers are large, historically bug-dense C code, so the renderer and GPU processes only decode formats on a web-safe allow-list. That allow-list is a predicate — WebCore::isSupportedImageType(), declared in UTIRegistry.h — rather than a process-wide switch, and the UIProcess could not use a process-wide switch anyway, because it is the host application's process and the app legitimately opens arbitrary image files. The invariant therefore has to be re-established by hand at every UIProcess call site that consumes bytes which arrived from WebContent.

The angle: a compromised WebContent process can steer the host application's own process into the platform's full image-codec set — PSD, OpenEXR, camera RAW, JPEG 2000 — through a saved photo, a share-menu thumbnail, or an attributed-string attachment, entirely outside the sandbox that contains renderer decodes.

Per the commit message, the change copies the image out of shared memory and only saves it to the photo library if it is a supported image type.

The change touches three consumers across two layers — the two Cocoa UIProcess call sites that build platform image objects from renderer bytes, and WebCore's attributed-string reconstruction path that feeds a file wrapper to AppKit or UIKit — adding the isSupportedImageType() predicate at each, and on the iOS path additionally restructuring how the bytes are obtained.

On iOS, PageClientImpl::saveImageToLibrary() is reached from the SaveImageToLibrary(WebCore::SharedMemory::Handle handle, String authorizationToken) IPC message; WebPageProxyIOS.mm maps the handle and calls createSharedBuffer() before handing the result to the page client, which previously passed the raw bytes straight to UIImageDataWriteToSavedPhotosAlbum(). The patched version calls toNSData(imageBuffer->span()) to materialize the bytes first, then sniffs the copy, so the type check and the photo-library write observe the same buffer rather than re-reading a mapping the sending process may still hold writable. On macOS, WebContextMenuProxyMac.mm built the share-menu thumbnail with [[NSImage alloc] initWithData:...toNSData()] over hitTestData.imageSharedMemory. In WebCore, AttributedString::nsAttributedString()'s TextAttachmentFileWrapper decode path placed raw attachment bytes into an NSFileWrapper backing an NSTextAttachment, which AppKit or UIKit decodes when the attachment is later rendered. A new DropsUnsupportedImageData test asserts that round-tripped attachment contents no longer equal the original Photoshop bytes.

  Before (iOS save path):                 After:
  SaveImageToLibrary(handle)              SaveImageToLibrary(handle)
    └─► map shared memory                   └─► map shared memory
          └─► createSharedBuffer()                └─► toNSData(span())   (copy out)
                └─► UIImageDataWrite...                 └─► isSupportedImageType?
                      └─ ImageIO selects                      ├─ no  ──► drop
                         ANY installed codec                  └─ yes ──► UIImageDataWrite...

A containment allow-list enforced per call site rather than per process, with three consumers of cross-boundary data omitting it entirely.

Where this lives. The UIProcess is the host application itself — Safari, or any WKWebView embedder. It is not sandboxed the way WebContent and the GPU process are, because it has to do application things: open files the user picks, talk to the window server, write to the photo library. Every byte that reaches it from WebContent crosses a privilege boundary in the direction that matters.

Uniform Type Identifiers and format sniffing. A UTI is the platform's type identifier for a piece of data (public.png, public.jpeg, com.adobe.photoshop-image). ImageIO does not require the caller to name the format: handed a byte buffer, it sniffs the magic bytes and selects whichever installed codec matches. That is convenient for an application and dangerous at a trust boundary, because the caller does not choose which parser runs — the data does.

The web-safe subset. WebCore::isSupportedImageType() names the UTIs the web platform is expected to decode. The set the operating system ships is much larger: Photoshop documents, OpenEXR, camera RAW variants, JPEG 2000 and others are all decodable by ImageIO and none of them are reachable from a normal web page. The gap between "what this platform can parse" and "what the web needs" is the whole point of the predicate.

Attributed strings and file wrappers. AttributedString is WebKit's IPC-serializable form of a rich-text run, used for editing, dictionary lookup and rich-text transfer; nsAttributedString() is its Cocoa reconstruction side. An NSTextAttachment backed by an NSFileWrapper carries arbitrary file contents that the text system decodes lazily when the attachment is drawn, which puts the decode a long way from the deserialization that accepted the bytes.

This is not a memory-safety defect; it is the absence of a containment invariant, and the bug type is attack-surface exposure across a sandbox boundary. In each of the three paths the decoder that ultimately runs is selected by ImageIO's format sniffer over attacker-supplied bytes, which means the reachable parser set is the platform's complete installed codec list rather than the web-safe subset. The commit message names PSD, OpenEXR, camera RAW and JPEG 2000 as examples; the DropsUnsupportedImageData test independently confirms that Photoshop (8BPS) data is outside the allow-list, since it asserts the round-tripped attachment contents no longer equal the original PSD bytes.

The attributed-string path is the least obvious of the three and the most interesting for an auditor, because the decode does not happen where the data is accepted. nsAttributedString() only builds the NSFileWrapper; the parse fires later, when AppKit or UIKit renders the attachment. A reviewer looking at the deserialization code sees no decoder call at all.

The iOS restructuring is worth noting separately from the allow-list itself. Checking a type in shared memory and then writing from that same mapping is a check-then-use split: the sending process may still hold the region writable, so the bytes that satisfy isSupportedImageType() need not be the bytes that reach UIImageDataWriteToSavedPhotosAlbum(). Materializing an NSData copy with toNSData(imageBuffer->span()) before sniffing collapses the two observations onto one buffer, which is the correct shape for any validate-then-consume sequence over a shared mapping.

What an attacker gains stops at reachability. The change establishes that renderer-controlled bytes could drive arbitrary installed codecs in the host application's process; it does not by itself establish a memory-safety primitive, because that requires a bug in one of those codecs. The value here is the surface delta, and it is a large one: the UIProcess is where an ImageIO parser bug is worth the most, since it is the process that holds the application's entitlements.

Before the fix, the multi-process design's image-decoding containment held in the renderer and GPU process but not at the UIProcess IPC boundary, which is where a successful codec exploit is most valuable.