Any time a string is converted to a floating point value, a specially crafted string can cause a heap overflow. This can lead to a denial of service attack via segmentation faults and possibly arbitrary code execution. Any program that converts input of unknown origin to floating point values (especially common when accepting JSON) are vulnerable.
A couple of PoCs (only affecting MRI).
# PoC
("0."+"1"*300000).to_f
$ ruby -e '("0."+"1"*300000).to_f'
[1] 9352 segmentation fault (core dumped) ruby -e '("1."+"1"*300000).to_f'
irb(main):001:0> require 'json'
=> true
irb(main):002:0> JSON.parse("[0."+"1"*300000+"]").to_f
[1] 9182 segmentation fault (core dumped) irb
MRI converted a decimal string to a float by reading the digits into a heap buffer sized from an estimate of the significant digits. Feed it a string far beyond what the estimate allowed, some 300,000 characters of 1 after the decimal point, and the parser wrote past the end of the allocation. Heap overflow, memory corruption, and a segfault soon after.
The PoC above calls .to_f on a string you typed yourself, which sounds harmless. The realistic path is JSON. A JSON document arrives over the network as text, and every number in it gets converted to a float by the parser, so a request body containing ["0.111..."] with 300k digits reached the vulnerable code through any endpoint that parsed untrusted JSON. That makes it a denial of service against any Ruby service that accepts request bodies: one request, one crashed worker. Remote code execution was raised as a possibility, since a heap overflow corrupts memory that something else may later use, but there were no public exploits.
The fix landed in the Ruby releases of late November 2013, patchlevels 1.9.3-p484 and 2.0.0-p353. Anything at or after those is safe, and I re-ran both PoCs on Ruby 2.6 and 3.0 while writing this: both parse the 300k digit string without complaint. Thirteen years of patches stand between that crash log above and today.
The lasting lesson sits outside Ruby. Parsers run on untrusted input, and the size of that input is part of your threat model. A request body limit would have stopped this attack at the door.
Published on by Stanley