Released May 12, 2025

CVE-2025-31223

CVSS 8 severity high CWE-119CWE-120 vector CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H

Overview

Impact:

Description: Processing maliciously crafted web content may lead to memory corruption

Researchers: Andreas Jaegersberger & Ro Achterberg of Nosebeard Labs

Key insight. The fast stringifier’s capacity check computed 1 + stringLength*6 + 1 in a 32-bit domain, so a huge string wrapped the check to a small value and the escaped output overran the buffer; an overflow-checked length restores the guard.

Patch Details

diff --git a/JSTests/stress/fast-stringifier-check-string-length.js b/JSTests/stress/fast-stringifier-check-string-length.js
new file mode 100644
index 000000000000..43f81c13407e
--- /dev/null
+++ b/JSTests/stress/fast-stringifier-check-string-length.js
@@ -0,0 +1,8 @@
+//@ skip if $memoryLimited
+var string = "\x00".repeat(715827883);
+try {
+    JSON.stringify(string);
+} catch { }
+try {
+    JSON.stringify(string);
+} catch { }
diff --git a/Source/JavaScriptCore/runtime/JSONObject.cpp b/Source/JavaScriptCore/runtime/JSONObject.cpp
index 00714548f0ff..ace6278f26ca 100644
--- a/Source/JavaScriptCore/runtime/JSONObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSONObject.cpp
@@ -1294,7 +1294,12 @@ void FastStringifier<CharType, bufferMode>::append(JSValue value)
             }
         }
 
-        if (!hasRemainingCapacity(1 + static_cast<size_t>(stringLength) * 6 + 1)) [[unlikely]] {
+        auto escapedLength = 1 + CheckedUint32 { stringLength } * 6 + 1;
+        if (escapedLength.hasOverflowed()) [[unlikely]] {
+            recordBufferFull();
+            return;
+        }
+        if (!hasRemainingCapacity(escapedLength.value())) [[unlikely]] {
             recordBufferFull();
             return;
         }

Background

Advisory: Apple Security Advisory

Attribute Value
CVE CVE-2025-31223
Bugzilla 289387
Component JSC
Bug Class IntegerOverflow
Severity medium
Commit 92e69a181eb39835…
Advisory Apple Advisory

Terminology

  • FastStringifier — JSC’s optimized JSON.stringify path that writes into a preallocated buffer, guarded by capacity checks.
  • Worst-case escaped length — Each character can expand to up to 6 output chars when escaped; the buffer must be checked against that bound.
  • CheckedUint32 — An overflow-trapping type; without it, stringLength*6 wraps and defeats the capacity check.

Root Cause Analysis

This fixes a heap buffer overflow in JavaScriptCore’s JSON fast stringifier caused by an overflowing worst-case-length capacity check. FastStringifier::append, before emitting an escaped string, must ensure the buffer has room for the worst case where each character escapes to up to 6 output characters (e.g. \u00XX) plus surrounding quotes: 1 + stringLength*6 + 1. Pre-patch this was computed as 1 + static_cast<size_t>(stringLength) * 6 + 1 and passed to hasRemainingCapacity, but the value is used in a 32-bit capacity domain, so for a very long string (the test uses 715,827,883 characters, whose *6 exceeds 2^32) the computation overflows and wraps to a small number. hasRemainingCapacity then returns true and the stringifier writes the escaped string past the end of the buffer — a heap buffer overflow. The fix computes auto escapedLength = 1 + CheckedUint32 { stringLength } * 6 + 1 and, if escapedLength.hasOverflowed(), calls recordBufferFull() and returns (aborting the fast path); otherwise it checks hasRemainingCapacity(escapedLength.value()). The restored invariant is that the worst-case escaped-length capacity computation cannot overflow undetected.

Attack Path

1. Build a huge string

Create a string long enough that stringLength*6 overflows 32 bits (~715M+ characters).

2. Call JSON.stringify

Stringify a value containing that string, entering the fast stringifier’s append path.

3. Overflow the capacity check

The 1 + stringLength*6 + 1 computation wraps to a small value, so the capacity check wrongly reports room.

4. Overflow the buffer

The escaped output is written past the buffer end — a heap buffer overflow in WebContent.

Impact Assessment

A heap buffer-overflow write in the WebContent process via JSON.stringify of a very large string — a strong, attacker-influenced corruption primitive that the advisory rates as memory corruption and a classic route toward controlled heap corruption.

Audit Directions

Same file: capacity math

Audit FastStringifier hasRemainingCapacity call sites and other *6 / worst-case length computations in JSONObject.cpp for overflow-safe arithmetic.

Preallocated-buffer writers

Grep for buffer-capacity checks multiplying a length by a constant expansion factor without Checked arithmetic.

Changed Functions

Function File Change Note
FastStringifier::append Source/JavaScriptCore/runtime/JSONObject.cpp modified Computes the worst-case escaped length with CheckedUint32 and aborts (recordBufferFull) on overflow before the hasRemainingCapacity check, preventing an under-sized capacity check.

Files Changed

Source Files

  • Source/JavaScriptCore/runtime/JSONObject.cpp

Test Files

  • JSTests/stress/fast-stringifier-check-string-length.js

Regression Test

The fix ships this proof-of-concept / regression test:

diff --git a/JSTests/stress/fast-stringifier-check-string-length.js b/JSTests/stress/fast-stringifier-check-string-length.js
new file mode 100644
index 000000000000..43f81c13407e
--- /dev/null
+++ b/JSTests/stress/fast-stringifier-check-string-length.js
@@ -0,0 +1,8 @@
+//@ skip if $memoryLimited
+var string = "\x00".repeat(715827883);
+try {
+    JSON.stringify(string);
+} catch { }
+try {
+    JSON.stringify(string);
+} catch { }