Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions ext/bson/read.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,31 @@ static VALUE pvt_get_double(byte_buffer_t *b);
static VALUE pvt_get_string(byte_buffer_t *b, const char *data_type);
static VALUE pvt_get_symbol(byte_buffer_t *b, VALUE rb_buffer, int argc, VALUE *argv);
static VALUE pvt_get_boolean(byte_buffer_t *b);
static VALUE pvt_read_field(byte_buffer_t *b, VALUE rb_buffer, uint8_t type, int argc, VALUE *argv);
static VALUE pvt_read_field(byte_buffer_t *b, VALUE rb_buffer, uint8_t type, int argc, VALUE *argv, int depth);
static VALUE pvt_get_hash_at_depth(int argc, VALUE *argv, VALUE self, int depth);
static VALUE pvt_get_array_at_depth(int argc, VALUE *argv, VALUE self, int depth);
static void pvt_check_nesting_depth(int depth);
static void pvt_skip_cstring(byte_buffer_t *b);
static size_t pvt_strnlen(const byte_buffer_t *b);

/* Maximum number of nested BSON documents or arrays the decoder will accept.
* Mirrors BSON::MAX_NESTING_DEPTH in lib/bson.rb. */
#define BSON_RUBY_MAX_NESTING_DEPTH 200

void pvt_raise_decode_error(volatile VALUE msg) {
VALUE klass = pvt_const_get_3("BSON", "Error", "BSONDecodeError");
rb_exc_raise(rb_exc_new_str(klass, msg));
}

/* Raise BSON::Error::BSONDecodeError if the depth exceeds the cap. */
void pvt_check_nesting_depth(int depth) {
if (depth > BSON_RUBY_MAX_NESTING_DEPTH) {
pvt_raise_decode_error(rb_sprintf(
"BSON document nesting depth exceeds maximum of %d",
BSON_RUBY_MAX_NESTING_DEPTH));
}
}

/**
* validate the buffer contains the amount of bytes the array / hash claimns
* and that it is null terminated
Expand Down Expand Up @@ -66,18 +82,19 @@ int32_t pvt_validate_length(byte_buffer_t *b)
}

/**
* Read a single field from a hash or array
* Read a single field from a hash or array. `depth` is the current nesting
* depth; nested documents/arrays bump it before recursing.
*/
VALUE pvt_read_field(byte_buffer_t *b, VALUE rb_buffer, uint8_t type, int argc, VALUE *argv)
VALUE pvt_read_field(byte_buffer_t *b, VALUE rb_buffer, uint8_t type, int argc, VALUE *argv, int depth)
{
switch(type) {
case BSON_TYPE_INT32: return pvt_get_int32(b);
case BSON_TYPE_INT64: return pvt_get_int64(b, argc, argv);
case BSON_TYPE_DOUBLE: return pvt_get_double(b);
case BSON_TYPE_STRING: return pvt_get_string(b, "String");
case BSON_TYPE_SYMBOL: return pvt_get_symbol(b, rb_buffer, argc, argv);
case BSON_TYPE_ARRAY: return rb_bson_byte_buffer_get_array(argc, argv, rb_buffer);
case BSON_TYPE_DOCUMENT: return rb_bson_byte_buffer_get_hash(argc, argv, rb_buffer);
case BSON_TYPE_ARRAY: return pvt_get_array_at_depth(argc, argv, rb_buffer, depth + 1);
case BSON_TYPE_DOCUMENT: return pvt_get_hash_at_depth(argc, argv, rb_buffer, depth + 1);
case BSON_TYPE_BOOLEAN: return pvt_get_boolean(b);
default:
{
Expand Down Expand Up @@ -397,13 +414,19 @@ static int pvt_is_dbref(VALUE doc) {
}

VALUE rb_bson_byte_buffer_get_hash(int argc, VALUE *argv, VALUE self){
return pvt_get_hash_at_depth(argc, argv, self, 1);
}

VALUE pvt_get_hash_at_depth(int argc, VALUE *argv, VALUE self, int depth){
VALUE doc = Qnil;
byte_buffer_t *b = NULL;
uint8_t type;
VALUE cDocument = pvt_const_get_2("BSON", "Document");
int32_t length;
char *start_ptr;

pvt_check_nesting_depth(depth);

TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b);

start_ptr = READ_PTR(b);
Expand All @@ -413,7 +436,7 @@ VALUE rb_bson_byte_buffer_get_hash(int argc, VALUE *argv, VALUE self){

while((type = pvt_get_type_byte(b)) != 0){
VALUE field = rb_bson_byte_buffer_get_cstring(self);
rb_hash_aset(doc, field, pvt_read_field(b, self, type, argc, argv));
rb_hash_aset(doc, field, pvt_read_field(b, self, type, argc, argv, depth));
RB_GC_GUARD(field);
}

Expand All @@ -430,12 +453,18 @@ VALUE rb_bson_byte_buffer_get_hash(int argc, VALUE *argv, VALUE self){
}

VALUE rb_bson_byte_buffer_get_array(int argc, VALUE *argv, VALUE self){
return pvt_get_array_at_depth(argc, argv, self, 1);
}

VALUE pvt_get_array_at_depth(int argc, VALUE *argv, VALUE self, int depth){
byte_buffer_t *b;
VALUE array = Qnil;
uint8_t type;
int32_t length;
char *start_ptr;

pvt_check_nesting_depth(depth);

TypedData_Get_Struct(self, byte_buffer_t, &rb_byte_buffer_data_type, b);

start_ptr = READ_PTR(b);
Expand All @@ -444,7 +473,7 @@ VALUE rb_bson_byte_buffer_get_array(int argc, VALUE *argv, VALUE self){
array = rb_ary_new();
while((type = pvt_get_type_byte(b)) != 0){
pvt_skip_cstring(b);
rb_ary_push(array, pvt_read_field(b, self, type, argc, argv));
rb_ary_push(array, pvt_read_field(b, self, type, argc, argv, depth));
}
RB_GC_GUARD(array);

Expand Down
23 changes: 23 additions & 0 deletions lib/bson.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ def self.ObjectId(string)
#
# @since 2.0.0
UTF8 = "UTF-8"

# Maximum number of nested BSON documents or arrays the decoder will accept.
# Prevents stack-overflow DoS on adversarial input. Matches libbson and the
# Go driver's ExtJSON parser.
MAX_NESTING_DEPTH = 200

# Bump the per-thread BSON decode nesting counter and raise if it exceeds
# MAX_NESTING_DEPTH. Pair every call with `leave_nesting_depth` in an
# `ensure` block. The bump is inlined at each callsite (rather than a
# block-yielding helper) to keep JRuby JVM stack frame counts low enough
# that the check fires before the JVM stack overflows on adversarial input.
def self.enter_nesting_depth
depth = (Thread.current[:_bson_nesting_depth] ||= 0) + 1
if depth > MAX_NESTING_DEPTH
raise Error::BSONDecodeError,
"BSON document nesting depth exceeds maximum of #{MAX_NESTING_DEPTH}"
end
Thread.current[:_bson_nesting_depth] = depth
end

def self.leave_nesting_depth
Thread.current[:_bson_nesting_depth] -= 1
end
end

require "bson/config"
Expand Down
21 changes: 13 additions & 8 deletions lib/bson/array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,20 @@ def from_bson(buffer, **options)
# @raise [ BSON::Error::BSONDecodeError ] if the expected number of
# bytes were not read from the buffer
def parse_array_from_buffer(buffer, **options)
new.tap do |array|
start_position = buffer.read_position
expected_byte_size = buffer.get_int32
parse_array_elements_from_buffer(array, buffer, **options)
actual_byte_size = buffer.read_position - start_position
if actual_byte_size != expected_byte_size
raise Error::BSONDecodeError,
"Expected array to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes"
BSON.enter_nesting_depth
begin
new.tap do |array|
start_position = buffer.read_position
expected_byte_size = buffer.get_int32
parse_array_elements_from_buffer(array, buffer, **options)
actual_byte_size = buffer.read_position - start_position
if actual_byte_size != expected_byte_size
raise Error::BSONDecodeError,
"Expected array to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes"
end
end
ensure
BSON.leave_nesting_depth
end
end

Expand Down
18 changes: 16 additions & 2 deletions lib/bson/ext_json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,13 @@ module ExtJSON
when Hash
parse_hash(value, **options)
when Array
value.map do |item|
parse_obj(item, **options)
BSON.enter_nesting_depth
begin
value.map do |item|
parse_obj(item, **options)
end
ensure
BSON.leave_nesting_depth
end
else
raise Error::ExtJSONParseError, "Unknown value type: #{value}"
Expand All @@ -136,6 +141,15 @@ module ExtJSON
end].freeze

module_function def parse_hash(hash, **options)
BSON.enter_nesting_depth
begin
return parse_hash_body(hash, **options)
ensure
BSON.leave_nesting_depth
end
end

module_function def parse_hash_body(hash, **options)
if hash.empty?
return {}
end
Expand Down
21 changes: 13 additions & 8 deletions lib/bson/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,22 @@ def maybe_dbref(hash)
#
# @return [ Hash ] the hash parsed from the buffer
def parse_hash_from_buffer(buffer, **options)
hash = Document.allocate
start_position = buffer.read_position
expected_byte_size = buffer.get_int32
BSON.enter_nesting_depth
begin
hash = Document.allocate
start_position = buffer.read_position
expected_byte_size = buffer.get_int32

parse_hash_contents(hash, buffer, **options)
parse_hash_contents(hash, buffer, **options)

actual_byte_size = buffer.read_position - start_position
return hash unless actual_byte_size != expected_byte_size
actual_byte_size = buffer.read_position - start_position
return hash unless actual_byte_size != expected_byte_size

raise Error::BSONDecodeError,
"Expected hash to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes"
raise Error::BSONDecodeError,
"Expected hash to take #{expected_byte_size} bytes but it took #{actual_byte_size} bytes"
ensure
BSON.leave_nesting_depth
end
end

# Given an empty hash and a byte buffer, parse the key/value pairs from
Expand Down
Loading
Loading