Oj gem CVE-2026-54899
MEDIUMSeverity by source
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/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
Attacker-supplied JSON reaches the bug remotely (AV:N) but only when the app reuses a parser and toggles symbol_keys (AC:H); no auth or UI needed; UAF most reliably crashes the worker (A:H) with possible limited memory disclosure/corruption (C:L/I:L).
Primary rating from Vendor (https://github.com/ohler55/oj).
CVSS VectorVendor: https://github.com/ohler55/oj
Lifecycle Timeline
3DescriptionCVE.org
Summary
Disabling symbol_keys on a reused Oj::Parser instance triggers a heap use-after-free. When symbol_keys is toggled from true to false, opt_symbol_keys_set frees the internal key cache (cache_free) but does not clear the pointer. The next parse call reads from the freed cache via cache_intern, producing a use-after-free.
Version
- Software: oj gem
- Affected: all versions with
ext/oj/usual.c - Latest tested: 3.17.1 (confirmed present)
Details
ext/oj/usual.c, opt_symbol_keys_set:
// usual.c:1043-1051
if (symbol_keys) {
d->key_cache = cache_create(...); // allocate
} else {
cache_free(d->key_cache); // free - but d->key_cache pointer not NULLed
}On the next parse call, cache_key → cache_intern reads from d->key_cache which now points to freed memory.
ASAN report:
==145265==ERROR: AddressSanitizer: heap-use-after-free on address 0x50b00001a318
READ of size 8 at 0x50b00001a318 thread T0
#0 cache_intern /ext/oj/cache.c:328
#1 cache_key /ext/oj/usual.c:161
#2 close_object /ext/oj/usual.c:285
#3 parse /ext/oj/parser.c:693
#4 parser_parse /ext/oj/parser.c:1408
freed by thread T0 here:
#0 free
#1 cache_free /ext/oj/cache.c:277
#2 opt_symbol_keys_set /ext/oj/usual.c:1051
#3 option /ext/oj/usual.c:1111
#4 parser_missing /ext/oj/parser.c:1362
0x50b00001a318 is 40 bytes inside freed 112-byte region [fd]fd fd fd fd fd fd fdReproduce
require 'oj'
p = Oj::Parser.new(:usual, symbol_keys: true)
p.symbol_keys = false
# frees cache without nulling pointer
p.parse('{"attacker":1}')
# UAF: reads freed cacheAnalysisAI
Heap use-after-free in the Oj Ruby JSON parser (versions prior to 3.17.3) is triggered when an application toggles the symbol_keys option from true to false on a reused Oj::Parser instance. The opt_symbol_keys_set function frees the internal key cache via cache_free but fails to NULL the d->key_cache pointer, so the next parse call dereferences freed memory through cache_intern, potentially leading to memory disclosure, crashes, or controlled corruption. No public exploit identified at time of analysis, and the issue is documented in GitHub Security Advisory GHSA-2cw7-v8ff-p88r.
Technical ContextAI
Oj (Optimized JSON) is a high-performance C-extension JSON parser/serializer for Ruby, widely used as a drop-in replacement for the standard JSON library and by Rails applications via multi_json. The flaw lives in the optimized 'usual' parser implementation (ext/oj/usual.c) which maintains a key_cache structure used by cache_intern to deduplicate and intern hash keys (as Symbol or String) during object parsing. The root cause is CWE-416 (Use After Free): opt_symbol_keys_set at ext/oj/usual.c:1043-1051 calls cache_free(d->key_cache) when disabling symbol keys but does not assign d->key_cache = NULL, leaving a dangling pointer that close_object → cache_key → cache_intern (cache.c:328) subsequently reads on the next parse, as confirmed by the AddressSanitizer trace provided in the advisory.
RemediationAI
Vendor-released patch: oj 3.17.3 - upgrade by pinning gem 'oj', '>= 3.17.3' in the Gemfile and running bundle update oj, then redeploy so the C extension is recompiled against the fixed source (see https://github.com/ohler55/oj/security/advisories/GHSA-2cw7-v8ff-p88r). If immediate upgrade is not possible, the targeted workaround is to avoid mutating symbol_keys on an existing Oj::Parser instance: construct a fresh Oj::Parser for each desired configuration, or set symbol_keys only once at construction; alternatively, route untrusted JSON through Oj.load / Oj.safe_load rather than a reused Oj::Parser, accepting the throughput cost of not reusing the parser object. Auditing application code and dependencies for any assignment like parser.symbol_keys = false on a long-lived parser is a quick way to confirm exposure.
Same weakness CWE-416 – Use After Free
View allSame technique Information Disclosure
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
GHSA-2cw7-v8ff-p88r