From be3b160d9dd67db5eec67bed8592cc14e5fe4c57 Mon Sep 17 00:00:00 2001 From: Naveed Khan Date: Thu, 13 Aug 2026 03:45:30 +0530 Subject: [PATCH] validate CSVFormat invariants when deserializing --- .../org/apache/commons/csv/CSVFormat.java | 9 +++++++++ .../org/apache/commons/csv/CSVFormatTest.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 6a686e468..891c424bf 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -24,6 +24,7 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InvalidObjectException; import java.io.NotActiveException; import java.io.ObjectInputStream; import java.io.OutputStream; @@ -2648,6 +2649,14 @@ private void printWithQuotes(final Reader reader, final Appendable appendable) t private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); writeLock = new Object(); + // The constructor validates these invariants, but deserialization bypasses it, so a crafted stream + // could yield a format the Builder rejects (for example the quote char equal to the delimiter, or + // QuoteMode.NONE with no escape char) that then misparses or throws in a print/parse callee. + try { + validate(); + } catch (final IllegalArgumentException e) { + throw (InvalidObjectException) new InvalidObjectException(e.getMessage()).initCause(e); + } } @Override diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index feebc408c..5c5056ec9 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -38,10 +38,12 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Reader; import java.io.StringReader; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.sql.ResultSet; @@ -1158,6 +1160,23 @@ void testSerialization() throws Exception { assertEquals(CSVFormat.DEFAULT.getIgnoreEmptyLines(), format.getIgnoreEmptyLines(), "empty lines"); } + @Test + void testSerializationRejectsInvalidInvariant() throws Exception { + // A format the Builder rejects: QuoteMode.NONE with no escape character. Reflection corrupts a fresh + // instance to build a serialized stream a hostile source could send. + final CSVFormat format = CSVFormat.DEFAULT.builder().get(); + final Field quoteModeField = CSVFormat.class.getDeclaredField("quoteMode"); + quoteModeField.setAccessible(true); + quoteModeField.set(format, QuoteMode.NONE); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(out)) { + oos.writeObject(format); + } + try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()))) { + assertThrows(InvalidObjectException.class, in::readObject); + } + } + @Test void testToString() {