This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cxf.git
The following commit(s) were added to refs/heads/main by this push:
new 1986c0b1e34 Escape JSON control characters (#3145)
1986c0b1e34 is described below
commit 1986c0b1e346513dc5b10a944854409316f73dd2
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Wed May 27 09:51:09 2026 +0100
Escape JSON control characters (#3145)
---
.../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");