Skip to content
Merged
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
41 changes: 41 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@

### Unreleased

### 2026-08-11 (3.0.0.rc1)

With the removal of the insecure `create_additions` option, `JSON.load` and `JSON.dump` are
now safe to use. Them being unsafe by default caused multiple security vulnerabilites in the past.

If you did depend on `create_additions`, the recommended migration is to [implement a custom serializer using
`JSON::Coder`](https://byroot.github.io/ruby/json/2025/08/02/whats-wrong-with-the-json-gem-api.html#the-create_additions-option).

All the mutable default options, such as `JSON.load_default_options` have been removed.
They were preventing Ractor compatiblity, and causing bug in libraries using JSON expecting the default behavior.
`JSON` methods now always behave the same unless monkey patched.

All methods options are now either keyword arguments or checked like keyword arguments, meaning
unknown options such as typos raise `ArgumentError`.

Duplicated keys are now rejected by default.

JavaScript comments in documents are no longer supported by default.

Numerous rarely used aliases have been removed.

* `JSON.load` defaults are now safe to use.
* All unknown options are now cause an `ArgumentError` rather than to be ignored.
* The `allow_comments` parsing option now default to `false`.
* The `allow_duplicate_key` option now defaults to `false`, for both parsing and generating JSON.
* Removed the `limit` positional argument of `JSON.dump`.
* Removed the `escape_slash` alias of `script_safe`.
* Removed `Kernel#j` and `Kernel#jj`.
* Removed `JSON.load_default_options`.
* Removed `JSON.unsafe_load_default_options`.
* Removed `JSON.dump_default_options`.
* Removed `JSON::State#[]` and `JSON::State#[]=`.
* Removed `JSON.unparse`.
* Removed `JSON.fast_generate`.
* Removed `JSON.fast_unparse`.
* Removed `JSON.pretty_unparse`.
* Removed `JSON.restore`.
* Removed `JSON::PRETTY_STATE_PROTOTYPE`.
* Removed the insecure `create_additions` option.
* Removed `JSON::GenericObject`.

### 2026-07-31 (2.21.2)

* Fix a use-after-free bug in `JSON::ResumableParser`. [GHSA-9hj4-r449-hfvc][CVE-2026-71847].
Expand Down
1 change: 0 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ gemspec
group :development do
gem "ruby_memcheck" if RUBY_PLATFORM =~ /linux/i
gem "bigdecimal"
gem "ostruct"
gem "rake"
gem "rake-compiler"
gem "test-unit"
Expand Down
88 changes: 4 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ UTF-16 surrogate pairs in order to be able to generate the whole range of
unicode code points.

All strings, that are to be encoded as JSON strings, should be UTF-8 byte
sequences on the Ruby side. To encode raw binary strings, that aren't UTF-8
encoded, please use the to\_json\_raw\_object method of String (which produces
an object, that contains a byte array) and decode the result on the receiving
endpoint.
sequences on the Ruby side.

## Installation

Expand Down Expand Up @@ -49,8 +46,7 @@ JSON.generate(data)
```

You can also use the `pretty_generate` method (which formats the output more
verbosely and nicely) or `fast_generate` (which doesn't do any of the security
checks generate performs, e. g. nesting deepness checks).
verbosely and nicely).

## Casting non native types

Expand Down Expand Up @@ -143,84 +139,12 @@ posts_json.map! { |post_json| JSON::Fragment.new(post_json) }
JSON.generate({ posts: posts_json, count: posts_json.count })
```

## Round-tripping arbitrary types

> [!CAUTION]
> You should never use `JSON.unsafe_load` nor `JSON.parse(str, create_additions: true)` to parse untrusted user input,
> as it can lead to remote code execution vulnerabilities.

To create a JSON document from a ruby data structure, you can call
`JSON.generate` like that:

```ruby
json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
# => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]"
```

To get back a ruby data structure from a JSON document, you have to call
JSON.parse on it:

```ruby
JSON.parse json
# => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"]
```

Note, that the range from the original data structure is a simple
string now. The reason for this is, that JSON doesn't support ranges
or arbitrary classes. In this case the json library falls back to call
`Object#to_json`, which is the same as `#to_s.to_json`.

It's possible to add JSON support serialization to arbitrary classes by
simply implementing a more specialized version of the `#to_json method`, that
should return a JSON object (a hash converted to JSON with `#to_json`) like
this (don't forget the `*a` for all the arguments):

```ruby
class Range
def to_json(*a)
{
'json_class' => self.class.name, # = 'Range'
'data' => [ first, last, exclude_end? ]
}.to_json(*a)
end
end
```

The hash key `json_class` is the class, that will be asked to deserialise the
JSON representation later. In this case it's `Range`, but any namespace of
the form `A::B` or `::A::B` will do. All other keys are arbitrary and can be
used to store the necessary data to configure the object to be deserialised.

If the key `json_class` is found in a JSON object, the JSON parser checks
if the given class responds to the `json_create` class method. If so, it is
called with the JSON object converted to a Ruby hash. So a range can
be deserialised by implementing `Range.json_create` like this:

```ruby
class Range
def self.json_create(o)
new(*o['data'])
end
end
```

Now it possible to serialise/deserialise ranges as well:

```ruby
json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
# => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]"
JSON.parse json
# => [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
# => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]"
JSON.unsafe_load json
# => [1, 2, {"a"=>3.141}, false, true, nil, 4..10]
```
## Pretty Printing

`JSON.generate` always creates the shortest possible string representation of a
ruby data structure in one line. This is good for data storage or network
protocols, but not so good for humans to read. Fortunately there's also
`JSON.pretty_generate` (or `JSON.pretty_generate`) that creates a more readable
`JSON.pretty_generate` that creates a more readable
output:

```ruby
Expand All @@ -245,10 +169,6 @@ output:
]
```

There are also the methods `Kernel#j` for generate, and `Kernel#jj` for
`pretty_generate` output to the console, that work analogous to Core Ruby's `p` and
the `pp` library's `pp` methods.

## Security

When parsing or serializing untrusted input, parser and generator options should never be user controlled.
Expand Down
2 changes: 1 addition & 1 deletion benchmark/standalone.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
JSON.dump(obj)
end
else
x.report('JSON.load(str)') do # max_nesting: false, allow_nan: true, allow_blank: true, create_additions: true
x.report('JSON.load(str)') do # max_nesting: false, allow_nan: true, allow_blank: true
JSON.load(str)
end
end
Expand Down
44 changes: 17 additions & 27 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@

/* ruby api and some helpers */

enum duplicate_key_action {
JSON_DEPRECATED = 0,
JSON_IGNORE,
JSON_RAISE,
};

typedef struct JSON_Generator_StateStruct {
VALUE indent;
VALUE space;
Expand All @@ -27,8 +21,7 @@ typedef struct JSON_Generator_StateStruct {
long depth;
long buffer_initial_length;

enum duplicate_key_action on_duplicate_key;

bool allow_duplicate_key;
bool as_json_single_arg;
bool allow_nan;
bool ascii_only;
Expand All @@ -41,7 +34,7 @@ static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_

static ID i_to_s, i_to_json, i_new, i_encode;
static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, sym_allow_duplicate_key,
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json, sym_sort_keys;
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys;


#define GET_STATE_TO(self, state) \
Expand Down Expand Up @@ -956,9 +949,8 @@ json_inspect_hash_with_mixed_keys(struct hash_foreach_arg *arg)
arg->mixed_keys_encountered = true;

JSON_Generator_State *state = arg->data->state;
if (state->on_duplicate_key != JSON_IGNORE) {
VALUE do_raise = state->on_duplicate_key == JSON_RAISE ? Qtrue : Qfalse;
rb_funcall(mJSON, rb_intern("on_mixed_keys_hash"), 2, arg->hash, do_raise);
if (!state->allow_duplicate_key) {
rb_funcall(mJSON, rb_intern("on_mixed_keys_hash"), 1, arg->hash);
}
}

Expand Down Expand Up @@ -1784,14 +1776,7 @@ static VALUE cState_sort_keys_set(VALUE self, VALUE value)
static VALUE cState_allow_duplicate_key_p(VALUE self)
{
GET_STATE(self);
switch (state->on_duplicate_key) {
case JSON_IGNORE:
return Qtrue;
case JSON_DEPRECATED:
return Qnil;
default:
return Qfalse;
}
return state->allow_duplicate_key ? Qtrue : Qfalse;
}

/*
Expand Down Expand Up @@ -1856,6 +1841,7 @@ static VALUE cState_buffer_initial_length_set(VALUE self, VALUE buffer_initial_l
struct configure_state_data {
JSON_Generator_State *state;
VALUE vstate; // Ruby object that owns the state, or Qfalse if stack-allocated
VALUE unknown_keywords;
};

static inline void state_write_value(struct configure_state_data *data, VALUE *field, VALUE value)
Expand Down Expand Up @@ -1883,9 +1869,8 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
else if (key == sym_depth) { state->depth = depth_config(val); }
else if (key == sym_buffer_initial_length) { buffer_initial_length_set(state, val); }
else if (key == sym_script_safe) { state->script_safe = RTEST(val); }
else if (key == sym_escape_slash) { state->script_safe = RTEST(val); }
else if (key == sym_strict) { state->strict = RTEST(val); }
else if (key == sym_allow_duplicate_key) { state->on_duplicate_key = RTEST(val) ? JSON_IGNORE : JSON_RAISE; }
else if (key == sym_allow_duplicate_key) { state->allow_duplicate_key = RTEST(val); }
else if (key == sym_as_json) {
VALUE proc = RTEST(val) ? rb_convert_type(val, T_DATA, "Proc", "to_proc") : Qfalse;
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
Expand All @@ -1894,6 +1879,12 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
else if (key == sym_sort_keys) {
state_write_value(data, &state->sort_keys, normalize_sort_keys(val));
}
else {
if (!data->unknown_keywords) {
data->unknown_keywords = rb_obj_hide(rb_ary_new());
}
rb_ary_push(data->unknown_keywords, key);
}
return ST_CONTINUE;
}

Expand All @@ -1907,12 +1898,15 @@ static void configure_state(JSON_Generator_State *state, VALUE vstate, VALUE con

struct configure_state_data data = {
.state = state,
.vstate = vstate
.vstate = vstate,
.unknown_keywords = Qfalse,
};

// We assume in most cases few keys are set so it's faster to go over
// the provided keys than to check all possible keys.
rb_hash_foreach(config, configure_state_i, (VALUE)&data);

raise_argument_error_on_unknown_keywords(data.unknown_keywords);
}

static VALUE cState_configure(VALUE self, VALUE opts)
Expand Down Expand Up @@ -2006,9 +2000,6 @@ void Init_generator(void)
rb_define_method(cState, "script_safe", cState_script_safe, 0);
rb_define_method(cState, "script_safe?", cState_script_safe, 0);
rb_define_method(cState, "script_safe=", cState_script_safe_set, 1);
rb_define_alias(cState, "escape_slash", "script_safe");
rb_define_alias(cState, "escape_slash?", "script_safe?");
rb_define_alias(cState, "escape_slash=", "script_safe=");
rb_define_method(cState, "strict", cState_strict, 0);
rb_define_method(cState, "strict?", cState_strict, 0);
rb_define_method(cState, "strict=", cState_strict_set, 1);
Expand Down Expand Up @@ -2050,7 +2041,6 @@ void Init_generator(void)
sym_depth = ID2SYM(rb_intern("depth"));
sym_buffer_initial_length = ID2SYM(rb_intern("buffer_initial_length"));
sym_script_safe = ID2SYM(rb_intern("script_safe"));
sym_escape_slash = ID2SYM(rb_intern("escape_slash"));
sym_strict = ID2SYM(rb_intern("strict"));
sym_as_json = ID2SYM(rb_intern("as_json"));
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
Expand Down
13 changes: 13 additions & 0 deletions ext/json/ext/json.h
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,17 @@ static inline VALUE json_rb_catch_obj(VALUE tag, VALUE (*func)(VALUE args), VALU

#endif // JSON_TRUFFLERUBY_RB_CATCH_BUG

static inline void raise_argument_error_on_unknown_keywords(VALUE unknown_keywords)
{
if (RB_UNLIKELY(unknown_keywords)) {
if (RARRAY_LEN(unknown_keywords) == 1) {
rb_raise(rb_eArgError, "unknown keyword: %" PRIsVALUE, RARRAY_AREF(unknown_keywords, 0));
}
else {
VALUE keywords = rb_ary_join(unknown_keywords, rb_utf8_str_new_cstr(", "));
rb_raise(rb_eArgError, "unknown keywords: %" PRIsVALUE, keywords);
}
}
}

#endif // _JSON_H_
Loading
Loading