This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/json-count-backslashes in repository https://gitbox.apache.org/repos/asf/cxf.git
commit e5b44f3e64067b7d23c61ec60cd1f2207ab06a4f Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon May 25 10:45:46 2026 +0100 Fix bug in JSON parsing relating to escaped backslashes --- .../json/basic/JsonMapObjectReaderWriter.java | 12 +++++- .../json/basic/JsonMapObjectReaderWriterTest.java | 43 ++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java index a99e7c43e73..f6c8abe4145 100644 --- a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java +++ b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java @@ -309,7 +309,17 @@ public class JsonMapObjectReaderWriter { nextCurlyBracketIndex = i; break; } else if (currentChar == DQUOTE) { - if (i > from && json.charAt(i - 1) == ESCAPE) { + // Count how many consecutive backslashes precede this quote. + // An odd count means the quote itself is escaped (e.g. \"); + // an even count means the backslashes are paired escape sequences + // and the quote is a real string delimiter (e.g. \\" = escaped \ + closing "). + int backslashCount = 0; + int k = i - 1; + while (k >= from && json.charAt(k) == ESCAPE) { + backslashCount++; + k--; + } + if (backslashCount % 2 != 0) { continue; } inString = !inString; diff --git a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java index 9c66add08c1..c2908b1a75d 100644 --- a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java +++ b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java @@ -196,6 +196,49 @@ public class JsonMapObjectReaderWriterTest { assertEquals("a\\", entry.getValue()); } + /** + * Regression test for a bug in {@code getNextSepCharIndex}: the method only checks whether + * the single character immediately before a {@code "} is a backslash when deciding whether + * the quote is escaped. That single-character look-back is wrong when a string ends with + * {@code \\} (an escaped backslash): the second {@code \} is mistaken for an escape prefix + * of the closing {@code "}, so the parser never exits "in-string" mode, swallows the + * subsequent comma, and absorbs the rest of the JSON (including any following keys) into + * the value of the preceding key. + * + * <p>Correct behaviour: {@code "\\"} in JSON is a string whose value is a single backslash + * {@code \}. The {@code "} that closes it must <em>not</em> be treated as escaped. + */ + @Test + public void testReadStringValueEndingWithEscapedBackslashNotLastKey() throws Exception { + // JSON: {"a":"\\","b":"w"} + // "a" has value \ (single backslash); "b" has value w. + // Bug: getNextSepCharIndex sees \ before the closing " of "\\" and skips + // that quote, causing "b" to be swallowed into the value of "a". + String json = "{\"a\":\"\\\\\",\"b\":\"w\"}"; + Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json); + assertEquals(2, map.size()); + assertEquals("\\", map.get("a")); + assertEquals("w", map.get("b")); + } + + /** + * Same bug as {@link #testReadStringValueEndingWithEscapedBackslashNotLastKey} but with a + * security-relevant follow-on key, matching the attack scenario described in the audit: + * a crafted value ending in {@code \\} causes a subsequent key such as {@code "admin"} to + * disappear from the parsed map. + */ + @Test + public void testReadStringValueEndingWithEscapedBackslashDropsSubsequentKey() throws Exception { + // JSON: {"role":"user\\","admin":true} + // "role" value is user\ (user + single backslash); "admin" value is Boolean.TRUE. + // Bug: "admin" key is consumed as part of the "role" value and absent from the result. + String json = "{\"role\":\"user\\\\\",\"admin\":true}"; + Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json); + assertEquals(2, map.size()); + assertEquals("user\\", map.get("role")); + assertEquals(Boolean.TRUE, map.get("admin")); + } + @Test public void testAlreadyEscapedBackslash() throws Exception { JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
