Skip to content

Commit 0be6006

Browse files
committed
Fix the parser to not accept invalid escapes
Only `"\/bfnrtu` are valid after a backslash.
1 parent 3b473ff commit 0be6006

File tree

9 files changed

+53
-30
lines changed

9 files changed

+53
-30
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
### Unreleased
44

5+
* Fixed the parser to no longer ignore invalid escapes in strings.
6+
Only `\"`, `\\`, `\b`, `\f`, `\n`, `\r`, `\t` and `\u` are valid JSON escapes.
7+
58
### 2025-11-07 (2.16.0)
69

710
* Deprecate `JSON::State#[]` and `JSON::State#[]=`. Consider using `JSON::Coder` instead.

ext/json/ext/parser/parser.c

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -639,44 +639,43 @@ static inline VALUE json_string_fastpath(JSON_ParserState *state, const char *st
639639
static VALUE json_string_unescape(JSON_ParserState *state, const char *string, const char *stringEnd, bool is_name, bool intern, bool symbolize)
640640
{
641641
size_t bufferSize = stringEnd - string;
642-
const char *p = string, *pe = string, *unescape, *bufferStart;
642+
const char *p = string, *pe = string, *bufferStart;
643643
char *buffer;
644-
int unescape_len;
645-
char buf[4];
646644

647645
VALUE result = rb_str_buf_new(bufferSize);
648646
rb_enc_associate_index(result, utf8_encindex);
649647
buffer = RSTRING_PTR(result);
650648
bufferStart = buffer;
651649

650+
#define APPEND_CHAR(chr) *buffer++ = chr; p = ++pe;
651+
652652
while (pe < stringEnd && (pe = memchr(pe, '\\', stringEnd - pe))) {
653-
unescape = (char *) "?";
654-
unescape_len = 1;
655653
if (pe > p) {
656654
MEMCPY(buffer, p, char, pe - p);
657655
buffer += pe - p;
658656
}
659657
switch (*++pe) {
658+
case '"':
659+
case '/':
660+
p = pe; // nothing to unescape just need to skip the backslash
661+
break;
662+
case '\\':
663+
APPEND_CHAR('\\');
664+
break;
660665
case 'n':
661-
unescape = (char *) "\n";
666+
APPEND_CHAR('\n');
662667
break;
663668
case 'r':
664-
unescape = (char *) "\r";
669+
APPEND_CHAR('\r');
665670
break;
666671
case 't':
667-
unescape = (char *) "\t";
668-
break;
669-
case '"':
670-
unescape = (char *) "\"";
671-
break;
672-
case '\\':
673-
unescape = (char *) "\\";
672+
APPEND_CHAR('\t');
674673
break;
675674
case 'b':
676-
unescape = (char *) "\b";
675+
APPEND_CHAR('\b');
677676
break;
678677
case 'f':
679-
unescape = (char *) "\f";
678+
APPEND_CHAR('\f');
680679
break;
681680
case 'u':
682681
if (pe > stringEnd - 5) {
@@ -714,18 +713,23 @@ static VALUE json_string_unescape(JSON_ParserState *state, const char *string, c
714713
break;
715714
}
716715
}
717-
unescape_len = convert_UTF32_to_UTF8(buf, ch);
718-
unescape = buf;
716+
717+
char buf[4];
718+
int unescape_len = convert_UTF32_to_UTF8(buf, ch);
719+
MEMCPY(buffer, buf, char, unescape_len);
720+
buffer += unescape_len;
721+
p = ++pe;
719722
}
720723
break;
721724
default:
722-
p = pe;
723-
continue;
725+
if ((unsigned char)*pe < 0x20) {
726+
raise_parse_error_at("invalid ASCII control character in string: %s", state, pe - 1);
727+
}
728+
raise_parse_error_at("invalid escape character in string: %s", state, pe - 1);
729+
break;
724730
}
725-
MEMCPY(buffer, unescape, char, unescape_len);
726-
buffer += unescape_len;
727-
p = ++pe;
728731
}
732+
#undef APPEND_CHAR
729733

730734
if (stringEnd > p) {
731735
MEMCPY(buffer, p, char, stringEnd - p);
@@ -976,9 +980,6 @@ static inline VALUE json_parse_string(JSON_ParserState *state, JSON_ParserConfig
976980
case '\\': {
977981
state->cursor++;
978982
escaped = true;
979-
if ((unsigned char)*state->cursor < 0x20) {
980-
raise_parse_error("invalid ASCII control character in string: %s", state);
981-
}
982983
break;
983984
}
984985
default:

java/src/json/ext/StringDecoder.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ private void handleEscapeSequence(ThreadContext context) throws IOException {
6969
case 't':
7070
append('\t');
7171
break;
72+
case '/':
73+
append('/');
74+
break;
75+
case '"':
76+
append('"');
77+
break;
78+
case '\\':
79+
append('\\');
80+
break;
7281
case 'u':
7382
ensureMin(context, 4);
7483
int cp = readHex(context);
@@ -81,8 +90,8 @@ private void handleEscapeSequence(ThreadContext context) throws IOException {
8190
writeUtf8Char(cp);
8291
}
8392
break;
84-
default: // '\\', '"', '/'...
85-
quoteStart();
93+
default:
94+
throw invalidEscape(context);
8695
}
8796
}
8897

@@ -174,4 +183,12 @@ protected RaiseException invalidUtf8(ThreadContext context) {
174183
return Utils.newException(context, Utils.M_PARSER_ERROR,
175184
context.runtime.newString(message));
176185
}
186+
187+
protected RaiseException invalidEscape(ThreadContext context) {
188+
ByteList message = new ByteList(
189+
ByteList.plain("invalid escape character in string: "));
190+
message.append(src, charStart, srcEnd - charStart);
191+
return Utils.newException(context, Utils.M_PARSER_ERROR,
192+
context.runtime.newString(message));
193+
}
177194
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

test/json/json_fixtures_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ class JSONFixturesTest < Test::Unit::TestCase
1010
source = File.read(f)
1111
define_method("test_#{name}") do
1212
assert JSON.parse(source), "Did not pass for fixture '#{File.basename(f)}': #{source.inspect}"
13+
rescue JSON::ParserError
14+
raise "#{File.basename(f)} parsing failure"
1315
end
1416
end
1517

test/json/json_parser_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,8 +510,8 @@ def test_backslash
510510
data = ['"']
511511
assert_equal data, parse(json)
512512
#
513-
json = '["\\\'"]'
514-
data = ["'"]
513+
json = '["\\/"]'
514+
data = ["/"]
515515
assert_equal data, parse(json)
516516

517517
json = '["\/"]'

0 commit comments

Comments
 (0)