Oj gem CVE-2026-54902
MEDIUMSeverity by source
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Remote attacker-controlled JSON reaches a SAJ handler over the network without auth (AV:N/PR:N/UI:N); exploitation needs a long key plus GC inside the callback, so AC:H; demonstrated impact is process crash (A:H), no confirmed C/I.
Primary rating from Vendor (https://github.com/ohler55/oj).
CVSS VectorVendor: https://github.com/ohler55/oj
Lifecycle Timeline
3DescriptionCVE.org
Summary
Oj::Parser in SAJ mode does not protect cached object keys (≥ 35 bytes) from garbage collection. A Ruby callback that triggers GC inside hash_end can cause the key string to be reclaimed while the C parser still holds a pointer to it. The subsequent access to the freed string VALUE results in a segfault, confirmed by an RIP pointing to address 0x4242 (a canary-style pattern suggesting control over the freed memory's content).
Version
- Software: oj gem
- Affected: all versions with
ext/oj/saj2.c/ext/oj/parser.c - Latest tested: 3.17.1 (confirmed present)
Details
Short keys (≤ 34 bytes) are stored inline on the C stack and are safe. Long keys (≥ 35 bytes) are stored as heap-allocated Ruby String objects passed to rb_funcall as the key argument. Between the key being resolved and the callback completing, a GC triggered inside the callback (e.g. GC.start) can collect the key String, leaving a dangling VALUE.
Crash output:
long_key_trigger
[BUG] Segmentation fault at 0x0000000000004242
close_object+0x260 /ext/oj/usual.c:405 (calls rb_funcall with freed key)
parse+0x11ff /ext/oj/parser.c:693
parser_parse+0x145 /ext/oj/parser.c:1408
RIP: 0x7fd1b46d68b7 RDI: 0x0000000000004242 (freed key VALUE)
R12: 0x0000000000004242The freed VALUE 0x4242 shows the attacker-controlled content of the key string was loaded as a pointer - a classic use-after-free indicator.
Reproduce
require 'oj'
class H < Oj::Saj
def add_value(value, key)
GC.start(full_mark: true, immediate_sweep: true) if key == 'x'
end
def hash_start(key); end
def hash_end(key); end
end
p = Oj::Parser.new(:saj)
p.handler = H.new
p.parse('{"' + 'A' * 35 + '":{"x":1}}')
# long outer key, GC fires on inner keyAnalysisAI
Use-after-free in the Oj Ruby JSON gem's SAJ parser allows an attacker who can influence parsed JSON content and the SAJ callback handler to crash the Ruby process and potentially corrupt memory. Oj::Parser fails to protect heap-allocated cached object keys of 35 bytes or more from garbage collection, so a GC cycle triggered from inside a hash_end callback frees the key while C code still holds a dangling VALUE pointer. No public exploit identified at time of analysis, but a working reproducer is published in the GHSA advisory.
Technical ContextAI
Oj is a widely used high-performance JSON parser/serializer for Ruby implemented as a C extension (pkg:rubygems/oj). The bug lives in the SAJ (Simple API for JSON) streaming parser implemented in ext/oj/saj2.c and ext/oj/parser.c, with the offending rb_funcall site in ext/oj/usual.c:405 (close_object). Short keys up to 34 bytes are kept inline on the C stack and are unaffected; keys of 35 bytes or longer are materialized as Ruby String objects on the GC-managed heap and passed to user callbacks. Because the parser does not mark or pin that VALUE across the callback, any GC cycle reachable from Ruby code inside hash_end can reclaim the string, leaving the C parser with a freed VALUE that is subsequently dereferenced - the textbook CWE-416 Use-After-Free pattern. The crash artefact (RIP/RDI = 0x4242 matching the attacker-supplied 'A' key bytes) demonstrates that the freed slot's contents are attacker-influenced, which is what makes this more than a pure stability issue.
RemediationAI
Vendor-released patch: upgrade the oj gem to 3.17.3 or later (bundle update oj and pin '~> 3.17.3' in the Gemfile), which is the fixed version recorded in the GHSA advisory at https://github.com/ohler55/oj/security/advisories/GHSA-m578-w5vf-rfcm. Where an immediate upgrade is not possible, switch JSON parsing off Oj::Parser SAJ mode by using Oj.load (DOM mode) or the standard library 'json' gem for untrusted input - at the cost of higher memory usage on large documents - or audit SAJ handler callbacks to remove any code paths that can trigger GC (avoid GC.start, large allocations, and require/autoload inside hash_end/add_value), which is fragile because implicit allocations are hard to eliminate. If neither is feasible, reject or size-limit incoming JSON so that no object key can reach 35 bytes; this is a brittle control that breaks legitimate payloads with long keys and should only be a stopgap.
Same weakness CWE-416 – Use After Free
View allSame technique Denial Of Service
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-m578-w5vf-rfcm