You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cxf.apache.org by co...@apache.org on 2026/05/26 08:49:36 UTC
(cxf) 01/01: Escape JSON control characters
This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch coheigea/json-special-chars
in repository https://gitbox.apache.org/repos/asf/cxf.git
commit 43b49e8baa1bcb063c61378a4bacbddf3d8504e1
Author: Colm O hEigeartaigh <co...@apache.org>
AuthorDate: Tue May 26 09:49:07 2026 +0100
Escape JSON control characters
---
.../json/basic/JsonMapObjectReaderWriter.java | 12 +++++-
.../json/basic/JsonMapObjectReaderWriterTest.java | 44 ++++++++++++++++++++++
2 files changed, 55 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 474e0832745..3522a6a496c 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
@@ -487,8 +487,18 @@ public class JsonMapObjectReaderWriter {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
+ if (c < 0x20) {
+ // RFC 8259 section 7: all control characters (U+0000–U+001F) MUST be escaped.
+ switch (c) {
+ case '\b': sb.append("\\b"); break;
+ case '\t': sb.append("\\t"); break;
+ case '\n': sb.append("\\n"); break;
+ case '\f': sb.append("\\f"); break;
+ case '\r': sb.append("\\r"); break;
+ default: sb.append(String.format("\\u%04x", (int) c)); break;
+ }
// If we have " and the previous char was not \ then escape it
- if (c == '"' && (i == 0 || value.charAt(i - 1) != '\\')) {
+ } else if (c == '"' && (i == 0 || value.charAt(i - 1) != '\\')) {
sb.append('\\').append(c);
// If we have \ and the previous char was not \ and the next char is not an escaped char, then escape it
} else if (c == '\\' && (i == 0 || value.charAt(i - 1) != '\\')
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 c133de675db..77b20e410a0 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
@@ -31,6 +31,7 @@ import org.apache.cxf.helpers.CastUtils;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -327,6 +328,49 @@ public class JsonMapObjectReaderWriterTest {
assertEquals("hello", map.get("a"));
}
+ /**
+ * RFC 8259 section 7 requires that all control characters (U+0000–U+001F) in string
+ * values be escaped in JSON output. {@code escapeJson} only escapes {@code "} and
+ * {@code \}; every other control character is emitted verbatim, producing JSON that
+ * violates the specification and may be rejected or mishandled by strict parsers.
+ *
+ * <p>The three tests below cover the most security-relevant cases:
+ * <ol>
+ * <li>A raw line-feed (U+000A) must be escaped as {@code \n}.</li>
+ * <li>A raw horizontal-tab (U+0009) must be escaped as {@code \t}.</li>
+ * <li>A raw CR+LF sequence must have both bytes escaped — an unescaped CR+LF in a
+ * JSON value that is subsequently placed in an HTTP response header enables
+ * HTTP response splitting (header injection).</li>
+ * </ol>
+ */
+ @Test
+ public void testRawNewlineInValueIsEscapedInOutput() throws Exception {
+ // Bug: escapeJson passes U+000A through verbatim; correct output is \n (two chars).
+ Map<String, Object> map = Collections.singletonMap("msg", "line1\nline2");
+ String json = new JsonMapObjectReaderWriter().toJson(map);
+ assertFalse("Raw newline must not appear verbatim in JSON output", json.contains("\n"));
+ assertEquals("{\"msg\":\"line1\\nline2\"}", json);
+ }
+
+ @Test
+ public void testRawTabInValueIsEscapedInOutput() throws Exception {
+ // Bug: escapeJson passes U+0009 through verbatim; correct output is \t (two chars).
+ Map<String, Object> map = Collections.singletonMap("msg", "col1\tcol2");
+ String json = new JsonMapObjectReaderWriter().toJson(map);
+ assertFalse("Raw tab must not appear verbatim in JSON output", json.contains("\t"));
+ assertEquals("{\"msg\":\"col1\\tcol2\"}", json);
+ }
+
+ @Test
+ public void testCrLfInValueDoesNotEnableHttpResponseSplitting() throws Exception {
+ // Bug: neither \r nor \n is escaped, so a crafted value can inject arbitrary
+ // HTTP headers when the JSON output is placed in a response header field.
+ Map<String, Object> map = Collections.singletonMap("v", "ok\r\nX-Injected: evil");
+ String json = new JsonMapObjectReaderWriter().toJson(map);
+ assertFalse("Raw CR must not appear verbatim in JSON output", json.contains("\r"));
+ assertFalse("Raw LF must not appear verbatim in JSON output", json.contains("\n"));
+ }
+
@Test
public void testRejectInfinityNumericValue() {
assertInvalidNumericLiteral("Infinity");