From e8a528c2784eb260b088e77c4082434d0a0e0492 Mon Sep 17 00:00:00 2001 From: Knut Wannheden Date: Wed, 28 Oct 2026 09:39:11 +0100 Subject: [PATCH] Reject a literal 0x1A after the value in strict mode The parser marks the end of input with the character 0x1A, so a literal 0x1A in the input looked like the end of the document. The trailing-data check of MODE_RFC4627 and MODE_STRICTEST (JSONValue.parseStrict and isValidJsonStrict) therefore accepted {"a":1} followed by 0x1A and arbitrary garbage as {"a":1}, on the String, byte[], Reader and InputStream backends. The trailing-data check now asks the backend whether the input is exhausted. The String and byte[] parsers compare the position with the length, and the Reader parser (which also serves InputStream) records whether its last read returned -1. Modes that accept trailing data keep treating a literal 0x1A as the end of input. Fixes #302 (CVE-2026-113602). --- README.md | 1 + .../minidev/json/parser/JSONParserBase.java | 7 ++- .../minidev/json/parser/JSONParserMemory.java | 4 ++ .../minidev/json/parser/JSONParserReader.java | 8 +++ .../minidev/json/test/TestCVE2026113602.java | 52 +++++++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 json-smart/src/test/java/net/minidev/json/test/TestCVE2026113602.java diff --git a/README.md b/README.md index 60d95cb..0eb91e4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ So I do not use my json-smart anymore. I had fun with this project. If you want * JSONObject merge support overwrite as parameter. [PR 238](https://github.com/netplex/json-smart-v2/pull/238) * Add `JSONParser.ACCEPT_INCOMPLETE` to allow parsing partial and incomplete JSON without error [PR 254](https://github.com/netplex/json-smart-v2/pull/254) * Add multiple JSON syntax blocks in a single input [PR 272](https://github.com/netplex/json-smart-v2/pull/272) +* Fix CVE-2026-113602 for strict parsers: reject a literal 0x1A after the JSON value. [PR 305](https://github.com/netplex/json-smart-v2/pull/305) ### *V 2.5.2* (2025-02-12) diff --git a/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java b/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java index 76a2274..f0f8d46 100644 --- a/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java +++ b/json-smart/src/main/java/net/minidev/json/parser/JSONParserBase.java @@ -233,7 +233,9 @@ private T parseInner(JsonReaderI mapper) throws ParseException { result = readFirst(mapper); if (checkTaillingData) { if (!checkTaillingSpace) skipSpace(); - if (c != EOI) throw new ParseException(pos - 1, ERROR_UNEXPECTED_TOKEN, c); + // Every check that takes EOI for the end stops reading there. + // So a 0x1A that ended the value early is still in c. + if (!isEndOfInput()) throw new ParseException(pos - 1, ERROR_UNEXPECTED_TOKEN, c); } } catch (IOException e) { throw new ParseException(pos, e); @@ -303,6 +305,9 @@ protected Number parseNumber(String s) throws ParseException { protected abstract void read() throws IOException; + /** Whether the input is exhausted. {@code c == EOI} also holds after reading a literal 0x1A. */ + protected abstract boolean isEndOfInput(); + protected T readArray(JsonReaderI mapper) throws ParseException, IOException { if (c != '[') throw new RuntimeException("Internal Error"); if (limitJsonDepth && ++this.depth > MAX_DEPTH) { diff --git a/json-smart/src/main/java/net/minidev/json/parser/JSONParserMemory.java b/json-smart/src/main/java/net/minidev/json/parser/JSONParserMemory.java index bec5808..471314a 100644 --- a/json-smart/src/main/java/net/minidev/json/parser/JSONParserMemory.java +++ b/json-smart/src/main/java/net/minidev/json/parser/JSONParserMemory.java @@ -35,6 +35,10 @@ public JSONParserMemory(int permissiveMode) { super(permissiveMode); } + protected boolean isEndOfInput() { + return pos >= len; + } + protected void readNQString(boolean[] stop) throws IOException { int start = pos; skipNQString(stop); diff --git a/json-smart/src/main/java/net/minidev/json/parser/JSONParserReader.java b/json-smart/src/main/java/net/minidev/json/parser/JSONParserReader.java index f12df0a..255cf70 100644 --- a/json-smart/src/main/java/net/minidev/json/parser/JSONParserReader.java +++ b/json-smart/src/main/java/net/minidev/json/parser/JSONParserReader.java @@ -29,6 +29,7 @@ */ class JSONParserReader extends JSONParserStream { private Reader in; + private boolean endOfInput; public JSONParserReader(int permissiveMode) { super(permissiveMode); @@ -58,8 +59,13 @@ public T parse(Reader in, JsonReaderI mapper) throws ParseException { return super.parse(mapper); } + protected boolean isEndOfInput() { + return endOfInput; + } + protected void read() throws IOException { int i = in.read(); + endOfInput = i == -1; c = (i == -1) ? (char) EOI : (char) i; pos++; } @@ -67,6 +73,7 @@ protected void read() throws IOException { protected void readS() throws IOException { sb.append(c); int i = in.read(); + endOfInput = i == -1; if (i == -1) { c = EOI; } else { @@ -77,6 +84,7 @@ protected void readS() throws IOException { protected void readNoEnd() throws ParseException, IOException { int i = in.read(); + endOfInput = i == -1; if (i == -1) { this.c = EOI; if (super.acceptIncomplete) return; diff --git a/json-smart/src/test/java/net/minidev/json/test/TestCVE2026113602.java b/json-smart/src/test/java/net/minidev/json/test/TestCVE2026113602.java new file mode 100644 index 0000000..2f4e312 --- /dev/null +++ b/json-smart/src/test/java/net/minidev/json/test/TestCVE2026113602.java @@ -0,0 +1,52 @@ +package net.minidev.json.test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayInputStream; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import net.minidev.json.parser.JSONParser; +import net.minidev.json.parser.ParseException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** A literal 0x1A (ASCII SUB) must not pass for the end of input in strict mode. */ +public class TestCVE2026113602 { + + private static final char SUB = 0x1A; + + enum Backend { + STRING, + BYTE_ARRAY, + READER, + INPUT_STREAM; + + Object parseStrict(String json) throws Exception { + JSONParser p = new JSONParser(JSONParser.MODE_RFC4627); + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + switch (this) { + case STRING: + return p.parse(json); + case BYTE_ARRAY: + return p.parse(bytes); + case READER: + return p.parse(new StringReader(json)); + default: + return p.parse(new ByteArrayInputStream(bytes)); + } + } + } + + @ParameterizedTest + @EnumSource(Backend.class) + public void strictParserRejectsSubAfterValue(Backend backend) { + assertThrows(ParseException.class, () -> backend.parseStrict("{\"a\":1}" + SUB)); + assertThrows(ParseException.class, () -> backend.parseStrict("{\"a\":1}" + SUB + "garbage")); + } + + @ParameterizedTest + @EnumSource(Backend.class) + public void strictParserRejectsSubInString(Backend backend) { + assertThrows(ParseException.class, () -> backend.parseStrict("{\"a\":\"x" + SUB + "y\"}")); + } +}