From f6aa92821da4c9feef9a9c18a167bcdf275f92f6 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 1 Oct 2026 09:08:42 +0100 Subject: [PATCH] Fix BasicJsonParser's escape handling in nested elements BasicJsonParser tokenizes JSON into map entries (key-value pairs) and list entries (values). As part of this, any escaped characters are unescaped. When the values are themselves maps or lists, they are tokenized again. This multi-pass approach with every pass performing unescaping causes the escaped characters to be handled incorrectly. This commit updates BasicJsonParser to leave escape characters in place when its parsing characters nested within a list or map. The escaping is now only considered when parsing a scalar value. This ensures that unescaping is only performed once. Fixes gh-51907 --- .../boot/json/BasicJsonParser.java | 7 +- .../boot/json/AbstractJsonParserTests.java | 73 ++++++++++++++++++- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/BasicJsonParser.java b/core/spring-boot/src/main/java/org/springframework/boot/json/BasicJsonParser.java index dc01f6827b0..673fc608b72 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/BasicJsonParser.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/BasicJsonParser.java @@ -155,7 +155,12 @@ public class BasicJsonParser extends AbstractJsonParser { build.setLength(0); } else if (ch == '\\') { - tracking.set(Tracked.ESCAPE, 1); + if (tracking.in(Tracked.OBJECT, Tracked.LIST)) { + build.append(ch); + } + else { + tracking.set(Tracked.ESCAPE, 1); + } } else { build.append(ch); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java b/core/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java index c3c3b669825..bad3ba6266c 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/json/AbstractJsonParserTests.java @@ -175,10 +175,77 @@ abstract class AbstractJsonParserTests { } @Test - void escapeDoubleQuote() { - String input = "{\"foo\": \"\\\"bar\\\"\"}"; + void escapeBackslash() { + String input = """ + { + "a": "alpha\\\\" + } + """; Map map = this.parser.parseMap(input); - assertThat(map).containsEntry("foo", "\"bar\""); + assertThat(map).containsEntry("a", "alpha\\"); + } + + @Test + void escapeDoubleQuote() { + String input = """ + { + "a": "alpha\\\"" + } + """; + Map map = this.parser.parseMap(input); + assertThat(map).containsEntry("a", "alpha\""); + } + + @Test + void escapeBackslashInNestedMap() { + String input = """ + { + "outer": { + "a": "alpha\\\\" + } + } + """; + Map map = this.parser.parseMap(input); + assertThat(map).extractingByKey("outer", InstanceOfAssertFactories.MAP).containsEntry("a", "alpha\\"); + } + + @Test + void escapeDoubleQuoteInNestedMap() { + String input = """ + { + "outer": { + "a": "alpha\\\"" + } + } + """; + Map map = this.parser.parseMap(input); + assertThat(map).extractingByKey("outer", InstanceOfAssertFactories.MAP).containsEntry("a", "alpha\""); + } + + @Test + void escapeBackslashInNestedList() { + String input = """ + { + "list": [ + "alpha\\\\" + ] + } + """; + Map map = this.parser.parseMap(input); + assertThat(map).extractingByKey("list", InstanceOfAssertFactories.LIST).containsExactly("alpha\\"); + } + + @Test + void escapeDoubleQuoteInNestedList() { + String input = """ + { + "list": [ + "alpha\\\"" + ] + } + """; + Map map = this.parser.parseMap(input); + assertThat(map).extractingByKey("list", InstanceOfAssertFactories.LIST).containsExactly("alpha\""); } @Test