Impact
When undici parses a Set-Cookie header, it accepts any SameSite attribute value that contains Strict, Lax, or None as a substring, rather than the case-insensitive exact match specified by RFC 6265. Non-spec values are silently mapped to one of the three standard tokens:
- SameSite=NoneOfYourBusiness is parsed as None, the most permissive setting. - SameSite=StrictLax is parsed as Lax, a downgrade from Strict.
Affected applications are those that consume Set-Cookie headers from server responses (for example via undici's fetch or proxy code paths) and then forward or rely on the parsed sameSite attribute. A malicious or non-compliant server can coerce the consumer's view of a cookie's SameSite policy to a weaker value, silently degrading the SameSite enforcement the cookie is supposed to provide.
This was introduced in undici 5.15.0 when the cookies feature was added.
Patches
Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.
Workarounds
After parsing a Set-Cookie header, validate that the resulting sameSite attribute is one of 'Strict', 'Lax', or 'None' (exact, case-insensitive) before forwarding or relying on it.
Impact
Undici's HTTP/1.1 client is vulnerable to response queue poisoning on reused keep-alive sockets. An attacker-controlled upstream server can inject an unsolicited HTTP/1.1 response onto an idle socket after a request completes. When the client dispatches the next request on that socket, it associates the injected response with the new request, causing responses to be delivered to the wrong requests.
This requires an attacker-controlled or compromised upstream HTTP/1.1 server and keep-alive connection reuse.
Patches
Upgrade to undici v6.27.0, v7.28.0 or v8.5.0.
Workarounds
Disable keep-alive connection reuse by setting keepAliveTimeout: 0 on the Client or Pool.
Summary
Oj::Doc iterators (eachvalue, eachchild, eachleaf) are vulnerable to a heap use-after-free. When a Ruby block yielded during iteration calls doc.close or d.close, the document's heap memory is freed while the C iterator is still running. When control returns from the block, the iterator reads from the freed region, producing a use-after-free accessible from pure Ruby.
Version
- Software: oj gem - Affected: all versions with ext/oj/fast.c - Latest tested: 3.17.1 (confirmed present)
Details
The iterators in ext/oj/fast.c follow the pattern:
c // fast.c:1505 (doceachchild) static VALUE doceachchild(VALUE self, ...) { ... while (cur != NULL) { rbyield(...); // ← Ruby block executes here cur = cur->next; // ← cur is now freed if block called close() } }
rbyield can invoke arbitrary Ruby code, including calling close() on the Doc or any child node, which calls rubysizedxfree on the backing buffer. On return, the C code reads cur->next from the freed region. All three iterators are affected.
ASAN report (eachchild variant): ==253632==ERROR: AddressSanitizer: heap-use-after-free on address 0x5210000bd080 READ of size 8 at 0x5210000bd080 thread T0 #0 doceachchild /ext/oj/fast.c:1505 0x5210000bd080 is located 896 bytes inside of 4064-byte region [0x5210000bcd00, 0x5210000bdce0) freed by thread T0 here: #0 free #1 rubysizedxfree (libruby-3.3.so.3.3)
All three iterators trigger the same freed region (fd shadow bytes): 0x5210000bd080:[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Reproduce
ruby require 'oj' eachchild Oj::Doc.open('[1,2]') { |doc| doc.eachchild { |d| d.close } } eachvalue Oj::Doc.open('[1,2]') { |doc| doc.eachvalue { |v| doc.close } } eachleaf Oj::Doc.open('[1,[2]]') { |doc| doc.eachleaf { |d| d.close } }
Summary
Oj.dump in object mode is vulnerable to a heap buffer overflow when serializing Exception objects with a large :indent value. The serializer allocates a buffer sized for the object's attributes but does not account for the indent bytes added on each write. With indent: 5000, the accumulation of 5,000-byte indent strings overflows the 13,150-byte heap allocation, corrupting adjacent heap memory.
Version
- Software: oj gem - Affected: all versions with ext/oj/dump.h - Latest tested: 3.17.1 (confirmed present)
Details
ext/oj/dump.h, line 75–77:
c static void fillindent(Out out, int depth) { if (0 < out->opts->indent) { memset(out->buf + out->cur, ' ', (sizet)(out->opts->indent depth));
When dumping an Exception object in :object mode, dumpobjattrs calls fillindent repeatedly for each attribute. The buffer is pre-allocated based on the serialized content but not the indentation overhead. With indent: 5000 the indent block for a nested object exceeds the remaining buffer space, producing a heap-buffer-overflow of size 5,000 at the end of the allocated region.
ASAN report: ==101656==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x527000022c5e WRITE of size 5000 at 0x527000022c5e thread T0 #0 memset #1 fillindent /ext/oj/dump.h:77 #2 dumpobjattrs /ext/oj/dumpobject.c:552 #3 dumpobj /ext/oj/dumpobject.c:80 #4 ojdumpobjval /ext/oj/dumpobject.c:708 #5 ojdumpobjtojsonusingparams /ext/oj/dump.c:817 #6 dumpbody /ext/oj/oj.c:1429 #7 dump /ext/oj/oj.c:1480 0x527000022c5e is located 0 bytes after 13150-byte region [0x52700001f900, 0x527000022c5e)
Reproduce
ruby require "oj" obj = Oj.load('{"^o":"RuntimeError"}', mode: :object) Oj.dump(obj, mode: :object, indent: 5000)
Workarounds
This is at the discretion of the developer and not a public facing option so the workaround is the develop should not use extreme indents and should not offer the option for users to dump Ruby data with unlimited indentation size.
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. hashstart) 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, parserparse → parse:
c static VALUE parserparse(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 parserparse /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 rubysizedxfree (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 hashstart(key) return if @done; @done = true @json.replace('x' 1000000) # triggers String realloc, frees original buffer end
def hashend(key); end def arraystart(key); end def arrayend(key); end def addvalue(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)
Summary
JSON.dump(obj, io) and JSON::State#generate(obj, io) can write past the internal JSON generator buffer when a streamed object contains an attacker-controlled string near 16 KB. The issue is a heap out-of-bounds write in the IO-streaming path and is demonstrated as a reliable process crash / denial of service.
This was triaged on HackerOne as report #3785370. The issue was confirmed there and I was asked to open it here.
Details
Root cause is in ext/json/fbuffer/fbuffer.h, fbufferdoinccapa().
On the IO path, the buffer is grown to FBUFFERIOBUFFERSIZE (16383), but the early return checks total capacity instead of remaining capacity:
c if (RBUNLIKELY(fb->io)) { if (fb->capa < FBUFFERIOBUFFERSIZE) { fbufferrealloc(fb, FBUFFERIOBUFFERSIZE); } else { fbufferflush(fb); }
if (RBLIKELY(requested < fb->capa)) { return; } }
If fb->len already contains JSON syntax bytes, and a string flush has 16383 - fb->len <= requested < 16383, this check returns even though there is not enough space left. fbufferappendreserved() then writes past the buffer:
c MEMCPY(fb->ptr + fb->len, newstr, char, len);
The minimal fix is to compare against the remaining capacity:
diff - if (RBLIKELY(requested < fb->capa)) { + if (RBLIKELY(requested <= fb->capa - fb->len)) { return; }
PoC
ruby require "json" require "stringio"
io = StringIO.new big = "a" 16385 big[16382] = '"' # escapable byte near the buffer boundary
JSON.dump([big], io)
Verified results:
text Ruby 4.0.5 / bundled json 2.18.0: malloc(): invalid size (unsorted) .../json/common.rb:956: [BUG] Aborted
ruby/ruby master c78418b7a0 / json 2.19.8 / ASan: heap-buffer-overflow WRITE of size 16382 fbufferappendreserved ext/json/fbuffer/fbuffer.h:145 searchflush ext/json/generator/generator.c:139 convertUTF8toJSON ext/json/generator/generator.c:231 rawgeneratejsonstring ext/json/generator/generator.c:922 cStatemgenerate ext/json/generator/generator.c:1891
Control: the same data through JSON.dump([big]) without an IO argument returns normally. The bug is specific to the IO-streaming path.
Impact
A remote attacker can trigger a heap out-of-bounds write if they control a string field that an application serializes through JSON.dump(obj, io) or JSON::State#generate(obj, io). The demonstrated impact is reliable denial of service. I am not claiming code execution or information disclosure.