Deprecate unsafe default options of JSON.load

[Feature #19528]

Ref: https://bugs.ruby-lang.org/issues/19528

`load` is understood as the default method for serializer kind of libraries, and
the default options of `JSON.load` has caused many security vulnerabilities over the
years.

The plan is to do like YAML/Psych, deprecate these default options and direct
users toward using `JSON.unsafe_load` so at least it's obvious it should be
used against untrusted data.
This commit is contained in:
Jean Boussier 2024-10-24 16:51:25 +02:00 committed by Hiroshi SHIBATA
parent cc2e67a138
commit f2b8829df0
5 changed files with 263 additions and 81 deletions

View File

@ -49,18 +49,9 @@ module JSON
# level (absolute namespace path?). If there doesn't exist a constant at
# the given path, an ArgumentError is raised.
def deep_const_get(path) # :nodoc:
path.to_s.split(/::/).inject(Object) do |p, c|
case
when c.empty? then p
when p.const_defined?(c, true) then p.const_get(c)
else
begin
p.const_missing(c)
rescue NameError => e
raise ArgumentError, "can't get const #{path}: #{e}"
end
end
end
Object.const_get(path)
rescue NameError => e
raise ArgumentError, "can't get const #{path}: #{e}"
end
# Set the module _generator_ to be used by JSON.
@ -69,7 +60,7 @@ module JSON
@generator = generator
generator_methods = generator::GeneratorMethods
for const in generator_methods.constants
klass = deep_const_get(const)
klass = const_get(const)
modul = generator_methods.const_get(const)
klass.class_eval do
instance_methods(false).each do |m|
@ -403,6 +394,20 @@ module JSON
module_function :pretty_unparse
# :startdoc:
class << self
# Sets or returns default options for the JSON.unsafe_load method.
# Initially:
# opts = JSON.load_default_options
# opts # => {:max_nesting=>false, :allow_nan=>true, :allow_blank=>true, :create_additions=>true}
attr_accessor :unsafe_load_default_options
end
self.unsafe_load_default_options = {
:max_nesting => false,
:allow_nan => true,
:allow_blank => true,
:create_additions => true,
}
class << self
# Sets or returns default options for the JSON.load method.
# Initially:
@ -411,11 +416,162 @@ module JSON
attr_accessor :load_default_options
end
self.load_default_options = {
:max_nesting => false,
:allow_nan => true,
:allow_blank => true,
:create_additions => true,
:create_additions => nil,
}
# :call-seq:
# JSON.unsafe_load(source, proc = nil, options = {}) -> object
#
# Returns the Ruby objects created by parsing the given +source+.
#
# - Argument +source+ must be, or be convertible to, a \String:
# - If +source+ responds to instance method +to_str+,
# <tt>source.to_str</tt> becomes the source.
# - If +source+ responds to instance method +to_io+,
# <tt>source.to_io.read</tt> becomes the source.
# - If +source+ responds to instance method +read+,
# <tt>source.read</tt> becomes the source.
# - If both of the following are true, source becomes the \String <tt>'null'</tt>:
# - Option +allow_blank+ specifies a truthy value.
# - The source, as defined above, is +nil+ or the empty \String <tt>''</tt>.
# - Otherwise, +source+ remains the source.
# - Argument +proc+, if given, must be a \Proc that accepts one argument.
# It will be called recursively with each result (depth-first order).
# See details below.
# BEWARE: This method is meant to serialise data from trusted user input,
# like from your own database server or clients under your control, it could
# be dangerous to allow untrusted users to pass JSON sources into it.
# - Argument +opts+, if given, contains a \Hash of options for the parsing.
# See {Parsing Options}[#module-JSON-label-Parsing+Options].
# The default options can be changed via method JSON.unsafe_load_default_options=.
#
# ---
#
# When no +proc+ is given, modifies +source+ as above and returns the result of
# <tt>parse(source, opts)</tt>; see #parse.
#
# Source for following examples:
# source = <<~JSON
# {
# "name": "Dave",
# "age" :40,
# "hats": [
# "Cattleman's",
# "Panama",
# "Tophat"
# ]
# }
# JSON
#
# Load a \String:
# ruby = JSON.unsafe_load(source)
# ruby # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
#
# Load an \IO object:
# require 'stringio'
# object = JSON.unsafe_load(StringIO.new(source))
# object # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
#
# Load a \File object:
# path = 't.json'
# File.write(path, source)
# File.open(path) do |file|
# JSON.unsafe_load(file)
# end # => {"name"=>"Dave", "age"=>40, "hats"=>["Cattleman's", "Panama", "Tophat"]}
#
# ---
#
# When +proc+ is given:
# - Modifies +source+ as above.
# - Gets the +result+ from calling <tt>parse(source, opts)</tt>.
# - Recursively calls <tt>proc(result)</tt>.
# - Returns the final result.
#
# Example:
# require 'json'
#
# # Some classes for the example.
# class Base
# def initialize(attributes)
# @attributes = attributes
# end
# end
# class User < Base; end
# class Account < Base; end
# class Admin < Base; end
# # The JSON source.
# json = <<-EOF
# {
# "users": [
# {"type": "User", "username": "jane", "email": "jane@example.com"},
# {"type": "User", "username": "john", "email": "john@example.com"}
# ],
# "accounts": [
# {"account": {"type": "Account", "paid": true, "account_id": "1234"}},
# {"account": {"type": "Account", "paid": false, "account_id": "1235"}}
# ],
# "admins": {"type": "Admin", "password": "0wn3d"}
# }
# EOF
# # Deserializer method.
# def deserialize_obj(obj, safe_types = %w(User Account Admin))
# type = obj.is_a?(Hash) && obj["type"]
# safe_types.include?(type) ? Object.const_get(type).new(obj) : obj
# end
# # Call to JSON.unsafe_load
# ruby = JSON.unsafe_load(json, proc {|obj|
# case obj
# when Hash
# obj.each {|k, v| obj[k] = deserialize_obj v }
# when Array
# obj.map! {|v| deserialize_obj v }
# end
# })
# pp ruby
# Output:
# {"users"=>
# [#<User:0x00000000064c4c98
# @attributes=
# {"type"=>"User", "username"=>"jane", "email"=>"jane@example.com"}>,
# #<User:0x00000000064c4bd0
# @attributes=
# {"type"=>"User", "username"=>"john", "email"=>"john@example.com"}>],
# "accounts"=>
# [{"account"=>
# #<Account:0x00000000064c4928
# @attributes={"type"=>"Account", "paid"=>true, "account_id"=>"1234"}>},
# {"account"=>
# #<Account:0x00000000064c4680
# @attributes={"type"=>"Account", "paid"=>false, "account_id"=>"1235"}>}],
# "admins"=>
# #<Admin:0x00000000064c41f8
# @attributes={"type"=>"Admin", "password"=>"0wn3d"}>}
#
def unsafe_load(source, proc = nil, options = nil)
opts = if options.nil?
unsafe_load_default_options
else
unsafe_load_default_options.merge(options)
end
unless source.is_a?(String)
if source.respond_to? :to_str
source = source.to_str
elsif source.respond_to? :to_io
source = source.to_io.read
elsif source.respond_to?(:read)
source = source.read
end
end
if opts[:allow_blank] && (source.nil? || source.empty?)
source = 'null'
end
result = parse(source, opts)
recurse_proc(result, &proc) if proc
result
end
# :call-seq:
# JSON.load(source, proc = nil, options = {}) -> object
@ -439,6 +595,7 @@ module JSON
# BEWARE: This method is meant to serialise data from trusted user input,
# like from your own database server or clients under your control, it could
# be dangerous to allow untrusted users to pass JSON sources into it.
# If you must use it, use JSON.unsafe_load instead to make it clear.
# - Argument +opts+, if given, contains a \Hash of options for the parsing.
# See {Parsing Options}[#module-JSON-label-Parsing+Options].
# The default options can be changed via method JSON.load_default_options=.

View File

@ -474,6 +474,9 @@ case 26:
if (!NIL_P(klassname)) {
VALUE klass = rb_funcall(mJSON, i_deep_const_get, 1, klassname);
if (RTEST(rb_funcall(klass, i_json_creatable_p, 0))) {
if (json->deprecated_create_additions) {
rb_warn("JSON.load implicit support for `create_additions: true` is deprecated and will be removed in 3.0, use JSON.unsafe_load or explicitly pass `create_additions: true`");
}
*result = rb_funcall(klass, i_json_create, 1, *result);
}
}
@ -486,7 +489,7 @@ case 26:
#line 490 "parser.c"
#line 493 "parser.c"
enum {JSON_value_start = 1};
enum {JSON_value_first_final = 29};
enum {JSON_value_error = 0};
@ -494,7 +497,7 @@ enum {JSON_value_error = 0};
enum {JSON_value_en_main = 1};
#line 287 "parser.rl"
#line 290 "parser.rl"
static char *JSON_parse_value(JSON_Parser *json, char *p, char *pe, VALUE *result, int current_nesting)
@ -502,14 +505,14 @@ static char *JSON_parse_value(JSON_Parser *json, char *p, char *pe, VALUE *resul
int cs = EVIL;
#line 506 "parser.c"
#line 509 "parser.c"
{
cs = JSON_value_start;
}
#line 294 "parser.rl"
#line 297 "parser.rl"
#line 513 "parser.c"
#line 516 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -543,14 +546,14 @@ st0:
cs = 0;
goto _out;
tr2:
#line 239 "parser.rl"
#line 242 "parser.rl"
{
char *np = JSON_parse_string(json, p, pe, result);
if (np == NULL) { p--; {p++; cs = 29; goto _out;} } else {p = (( np))-1;}
}
goto st29;
tr3:
#line 244 "parser.rl"
#line 247 "parser.rl"
{
char *np;
if(pe > p + 8 && !strncmp(MinusInfinity, p, 9)) {
@ -570,7 +573,7 @@ tr3:
}
goto st29;
tr7:
#line 262 "parser.rl"
#line 265 "parser.rl"
{
char *np;
np = JSON_parse_array(json, p, pe, result, current_nesting + 1);
@ -578,7 +581,7 @@ tr7:
}
goto st29;
tr11:
#line 268 "parser.rl"
#line 271 "parser.rl"
{
char *np;
np = JSON_parse_object(json, p, pe, result, current_nesting + 1);
@ -586,7 +589,7 @@ tr11:
}
goto st29;
tr25:
#line 232 "parser.rl"
#line 235 "parser.rl"
{
if (json->allow_nan) {
*result = CInfinity;
@ -596,7 +599,7 @@ tr25:
}
goto st29;
tr27:
#line 225 "parser.rl"
#line 228 "parser.rl"
{
if (json->allow_nan) {
*result = CNaN;
@ -606,19 +609,19 @@ tr27:
}
goto st29;
tr31:
#line 219 "parser.rl"
#line 222 "parser.rl"
{
*result = Qfalse;
}
goto st29;
tr34:
#line 216 "parser.rl"
#line 219 "parser.rl"
{
*result = Qnil;
}
goto st29;
tr37:
#line 222 "parser.rl"
#line 225 "parser.rl"
{
*result = Qtrue;
}
@ -627,9 +630,9 @@ st29:
if ( ++p == pe )
goto _test_eof29;
case 29:
#line 274 "parser.rl"
#line 277 "parser.rl"
{ p--; {p++; cs = 29; goto _out;} }
#line 633 "parser.c"
#line 636 "parser.c"
switch( (*p) ) {
case 13: goto st29;
case 32: goto st29;
@ -870,7 +873,7 @@ case 28:
_out: {}
}
#line 295 "parser.rl"
#line 298 "parser.rl"
if (json->freeze) {
OBJ_FREEZE(*result);
@ -884,7 +887,7 @@ case 28:
}
#line 888 "parser.c"
#line 891 "parser.c"
enum {JSON_integer_start = 1};
enum {JSON_integer_first_final = 3};
enum {JSON_integer_error = 0};
@ -892,7 +895,7 @@ enum {JSON_integer_error = 0};
enum {JSON_integer_en_main = 1};
#line 315 "parser.rl"
#line 318 "parser.rl"
static char *JSON_parse_integer(JSON_Parser *json, char *p, char *pe, VALUE *result)
@ -900,15 +903,15 @@ static char *JSON_parse_integer(JSON_Parser *json, char *p, char *pe, VALUE *res
int cs = EVIL;
#line 904 "parser.c"
#line 907 "parser.c"
{
cs = JSON_integer_start;
}
#line 322 "parser.rl"
#line 325 "parser.rl"
json->memo = p;
#line 912 "parser.c"
#line 915 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -942,14 +945,14 @@ case 3:
goto st0;
goto tr4;
tr4:
#line 312 "parser.rl"
#line 315 "parser.rl"
{ p--; {p++; cs = 4; goto _out;} }
goto st4;
st4:
if ( ++p == pe )
goto _test_eof4;
case 4:
#line 953 "parser.c"
#line 956 "parser.c"
goto st0;
st5:
if ( ++p == pe )
@ -968,7 +971,7 @@ case 5:
_out: {}
}
#line 324 "parser.rl"
#line 327 "parser.rl"
if (cs >= JSON_integer_first_final) {
long len = p - json->memo;
@ -983,7 +986,7 @@ case 5:
}
#line 987 "parser.c"
#line 990 "parser.c"
enum {JSON_float_start = 1};
enum {JSON_float_first_final = 8};
enum {JSON_float_error = 0};
@ -991,7 +994,7 @@ enum {JSON_float_error = 0};
enum {JSON_float_en_main = 1};
#line 349 "parser.rl"
#line 352 "parser.rl"
static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *result)
@ -999,15 +1002,15 @@ static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *resul
int cs = EVIL;
#line 1003 "parser.c"
#line 1006 "parser.c"
{
cs = JSON_float_start;
}
#line 356 "parser.rl"
#line 359 "parser.rl"
json->memo = p;
#line 1011 "parser.c"
#line 1014 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -1065,14 +1068,14 @@ case 8:
goto st0;
goto tr9;
tr9:
#line 343 "parser.rl"
#line 346 "parser.rl"
{ p--; {p++; cs = 9; goto _out;} }
goto st9;
st9:
if ( ++p == pe )
goto _test_eof9;
case 9:
#line 1076 "parser.c"
#line 1079 "parser.c"
goto st0;
st5:
if ( ++p == pe )
@ -1133,7 +1136,7 @@ case 7:
_out: {}
}
#line 358 "parser.rl"
#line 361 "parser.rl"
if (cs >= JSON_float_first_final) {
VALUE mod = Qnil;
@ -1186,7 +1189,7 @@ case 7:
#line 1190 "parser.c"
#line 1193 "parser.c"
enum {JSON_array_start = 1};
enum {JSON_array_first_final = 17};
enum {JSON_array_error = 0};
@ -1194,7 +1197,7 @@ enum {JSON_array_error = 0};
enum {JSON_array_en_main = 1};
#line 438 "parser.rl"
#line 441 "parser.rl"
static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *result, int current_nesting)
@ -1208,14 +1211,14 @@ static char *JSON_parse_array(JSON_Parser *json, char *p, char *pe, VALUE *resul
*result = NIL_P(array_class) ? rb_ary_new() : rb_class_new_instance(0, 0, array_class);
#line 1212 "parser.c"
#line 1215 "parser.c"
{
cs = JSON_array_start;
}
#line 451 "parser.rl"
#line 454 "parser.rl"
#line 1219 "parser.c"
#line 1222 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -1254,7 +1257,7 @@ case 2:
goto st2;
goto st0;
tr2:
#line 415 "parser.rl"
#line 418 "parser.rl"
{
VALUE v = Qnil;
char *np = JSON_parse_value(json, p, pe, &v, current_nesting);
@ -1274,7 +1277,7 @@ st3:
if ( ++p == pe )
goto _test_eof3;
case 3:
#line 1278 "parser.c"
#line 1281 "parser.c"
switch( (*p) ) {
case 13: goto st3;
case 32: goto st3;
@ -1374,14 +1377,14 @@ case 12:
goto st3;
goto st12;
tr4:
#line 430 "parser.rl"
#line 433 "parser.rl"
{ p--; {p++; cs = 17; goto _out;} }
goto st17;
st17:
if ( ++p == pe )
goto _test_eof17;
case 17:
#line 1385 "parser.c"
#line 1388 "parser.c"
goto st0;
st13:
if ( ++p == pe )
@ -1437,7 +1440,7 @@ case 16:
_out: {}
}
#line 452 "parser.rl"
#line 455 "parser.rl"
if(cs >= JSON_array_first_final) {
return p + 1;
@ -1598,7 +1601,7 @@ static VALUE json_string_unescape(char *string, char *stringEnd, int intern, int
}
#line 1602 "parser.c"
#line 1605 "parser.c"
enum {JSON_string_start = 1};
enum {JSON_string_first_final = 8};
enum {JSON_string_error = 0};
@ -1606,7 +1609,7 @@ enum {JSON_string_error = 0};
enum {JSON_string_en_main = 1};
#line 630 "parser.rl"
#line 633 "parser.rl"
static int
@ -1627,15 +1630,15 @@ static char *JSON_parse_string(JSON_Parser *json, char *p, char *pe, VALUE *resu
VALUE match_string;
#line 1631 "parser.c"
#line 1634 "parser.c"
{
cs = JSON_string_start;
}
#line 650 "parser.rl"
#line 653 "parser.rl"
json->memo = p;
#line 1639 "parser.c"
#line 1642 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -1660,7 +1663,7 @@ case 2:
goto st0;
goto st2;
tr2:
#line 617 "parser.rl"
#line 620 "parser.rl"
{
*result = json_string_unescape(json->memo + 1, p, json->parsing_name || json-> freeze, json->parsing_name && json->symbolize_names);
if (NIL_P(*result)) {
@ -1670,14 +1673,14 @@ tr2:
{p = (( p + 1))-1;}
}
}
#line 627 "parser.rl"
#line 630 "parser.rl"
{ p--; {p++; cs = 8; goto _out;} }
goto st8;
st8:
if ( ++p == pe )
goto _test_eof8;
case 8:
#line 1681 "parser.c"
#line 1684 "parser.c"
goto st0;
st3:
if ( ++p == pe )
@ -1753,7 +1756,7 @@ case 7:
_out: {}
}
#line 652 "parser.rl"
#line 655 "parser.rl"
if (json->create_additions && RTEST(match_string = json->match_string)) {
VALUE klass;
@ -1888,10 +1891,16 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
}
tmp = ID2SYM(i_create_additions);
if (option_given_p(opts, tmp)) {
json->create_additions = RTEST(rb_hash_aref(opts, tmp));
} else {
json->create_additions = 0;
tmp = rb_hash_aref(opts, tmp);
if (NIL_P(tmp)) {
json->create_additions = 1;
json->deprecated_create_additions = 1;
} else {
json->create_additions = RTEST(tmp);
json->deprecated_create_additions = 0;
}
}
if (json->symbolize_names && json->create_additions) {
rb_raise(rb_eArgError,
"options :symbolize_names and :create_additions cannot be "
@ -1946,7 +1955,7 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
}
#line 1950 "parser.c"
#line 1959 "parser.c"
enum {JSON_start = 1};
enum {JSON_first_final = 10};
enum {JSON_error = 0};
@ -1954,7 +1963,7 @@ enum {JSON_error = 0};
enum {JSON_en_main = 1};
#line 858 "parser.rl"
#line 867 "parser.rl"
/*
@ -1972,16 +1981,16 @@ static VALUE cParser_parse(VALUE self)
GET_PARSER;
#line 1976 "parser.c"
#line 1985 "parser.c"
{
cs = JSON_start;
}
#line 875 "parser.rl"
#line 884 "parser.rl"
p = json->source;
pe = p + json->len;
#line 1985 "parser.c"
#line 1994 "parser.c"
{
if ( p == pe )
goto _test_eof;
@ -2015,7 +2024,7 @@ st0:
cs = 0;
goto _out;
tr2:
#line 850 "parser.rl"
#line 859 "parser.rl"
{
char *np = JSON_parse_value(json, p, pe, &result, 0);
if (np == NULL) { p--; {p++; cs = 10; goto _out;} } else {p = (( np))-1;}
@ -2025,7 +2034,7 @@ st10:
if ( ++p == pe )
goto _test_eof10;
case 10:
#line 2029 "parser.c"
#line 2038 "parser.c"
switch( (*p) ) {
case 13: goto st10;
case 32: goto st10;
@ -2114,7 +2123,7 @@ case 9:
_out: {}
}
#line 878 "parser.rl"
#line 887 "parser.rl"
if (cs >= JSON_first_final && p == pe) {
return result;

View File

@ -26,6 +26,7 @@ typedef struct JSON_ParserStruct {
char symbolize_names;
char freeze;
char create_additions;
char deprecated_create_additions;
} JSON_Parser;
#define GET_PARSER \

View File

@ -196,6 +196,9 @@ static char *JSON_parse_object(JSON_Parser *json, char *p, char *pe, VALUE *resu
if (!NIL_P(klassname)) {
VALUE klass = rb_funcall(mJSON, i_deep_const_get, 1, klassname);
if (RTEST(rb_funcall(klass, i_json_creatable_p, 0))) {
if (json->deprecated_create_additions) {
rb_warn("JSON.load implicit support for `create_additions: true` is deprecated and will be removed in 3.0, use JSON.unsafe_load or explicitly pass `create_additions: true`");
}
*result = rb_funcall(klass, i_json_create, 1, *result);
}
}
@ -783,10 +786,16 @@ static VALUE cParser_initialize(int argc, VALUE *argv, VALUE self)
}
tmp = ID2SYM(i_create_additions);
if (option_given_p(opts, tmp)) {
json->create_additions = RTEST(rb_hash_aref(opts, tmp));
} else {
json->create_additions = 0;
tmp = rb_hash_aref(opts, tmp);
if (NIL_P(tmp)) {
json->create_additions = 1;
json->deprecated_create_additions = 1;
} else {
json->create_additions = RTEST(tmp);
json->deprecated_create_additions = 0;
}
}
if (json->symbolize_names && json->create_additions) {
rb_raise(rb_eArgError,
"options :symbolize_names and :create_additions cannot be "

View File

@ -162,6 +162,12 @@ class JSONAdditionTest < Test::Unit::TestCase
assert_equal(/foo/i, JSON(JSON(/foo/i), :create_additions => true))
end
def test_deprecated_load_create_additions
assert_warning(/use JSON\.unsafe_load/) do
JSON.load(JSON.dump(Time.now))
end
end
def test_utc_datetime
now = Time.now
d = DateTime.parse(now.to_s) # usual case