diff --git a/CHANGES.md b/CHANGES.md index 8417ecd5..52ccfa47 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ### Unreleased +### 2026-03-18 (2.17.1.2) + +* Fix a format string injection vulnerability in JSON.parse(doc, allow_duplicate_key: false). + ### 2025-12-04 (2.17.1) * Fix a regression in parsing of unicode surogate pairs (`\uXX\uXX`) that could cause an invalid string to be returned. diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index c84c7ed6..b1aacc92 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -399,14 +399,9 @@ static void emit_parse_warning(const char *message, JSON_ParserState *state) #define PARSE_ERROR_FRAGMENT_LEN 32 -#ifdef RBIMPL_ATTR_NORETURN -RBIMPL_ATTR_NORETURN() -#endif -static void raise_parse_error(const char *format, JSON_ParserState *state) +static VALUE build_parse_error_message(const char *format, JSON_ParserState *state, long line, long column) { unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 3]; - long line, column; - cursor_position(state, &line, &column); const char *ptr = "EOF"; if (state->cursor && state->cursor < state->end) { @@ -441,11 +436,23 @@ static void raise_parse_error(const char *format, JSON_ParserState *state) VALUE msg = rb_sprintf(format, ptr); VALUE message = rb_enc_sprintf(enc_utf8, "%s at line %ld column %ld", RSTRING_PTR(msg), line, column); RB_GC_GUARD(msg); + return message; +} +static VALUE parse_error_new(VALUE message, long line, long column) +{ VALUE exc = rb_exc_new_str(rb_path2class("JSON::ParserError"), message); rb_ivar_set(exc, rb_intern("@line"), LONG2NUM(line)); rb_ivar_set(exc, rb_intern("@column"), LONG2NUM(column)); - rb_exc_raise(exc); + return exc; +} + +NORETURN(static) void raise_parse_error(const char *format, JSON_ParserState *state) +{ + long line, column; + cursor_position(state, &line, &column); + VALUE message = build_parse_error_message(format, state, line, column); + rb_exc_raise(parse_error_new(message, line, column)); } #ifdef RBIMPL_ATTR_NORETURN @@ -889,6 +896,11 @@ static void raise_duplicate_key_error(JSON_ParserState *state, VALUE duplicate_k rb_inspect(duplicate_key) ); + long line, column; + cursor_position(state, &line, &column); + rb_str_concat(message, build_parse_error_message("", state, line, column)) ; + rb_exc_raise(parse_error_new(message, line, column)); + raise_parse_error(RSTRING_PTR(message), state); RB_GC_GUARD(message); } diff --git a/lib/json/version.rb b/lib/json/version.rb index 4ed61c43..4835495e 100644 --- a/lib/json/version.rb +++ b/lib/json/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module JSON - VERSION = '2.17.1' + VERSION = '2.17.1.2' end diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 257e4f17..8d891e65 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -397,6 +397,13 @@ def test_parse_duplicate_key end end + def test_parse_duplicate_key_escape + error = assert_raise(ParserError) do + JSON.parse('{"%s%s%s%s":1,"%s%s%s%s":2}', allow_duplicate_key: false) + end + assert_match "%s%s%s%s", error.message + end + def test_some_wrong_inputs assert_raise(ParserError) { parse('[] bla') } assert_raise(ParserError) { parse('[] 1') }