diff --git a/examples/src/main/resources/org/quartz/examples/example13/instance1.properties b/examples/src/main/resources/org/quartz/examples/example13/instance1.properties index 646ca5ab4..2df78cf25 100644 --- a/examples/src/main/resources/org/quartz/examples/example13/instance1.properties +++ b/examples/src/main/resources/org/quartz/examples/example13/instance1.properties @@ -22,6 +22,13 @@ org.quartz.jobStore.misfireThreshold: 60000 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate + +# Optional: restrict which classes JDBCJobStore will deserialize from the database +# (job data maps, triggers, calendars) using a JEP-290 look-ahead ObjectInputFilter. +# It is off by default; when enabled, only classes matching the pattern are allowed. +# Tailor the allow-list to the classes your jobs actually persist, then reject the +# rest with "!*". The value uses java.io.ObjectInputFilter pattern syntax, e.g.: +#org.quartz.jobStore.driverDelegateInitString=objectInputFilter=java.lang.*;java.util.*;org.quartz.**;com.myapp.jobs.**;!* org.quartz.jobStore.useProperties=false org.quartz.jobStore.dataSource=myDS org.quartz.jobStore.tablePrefix=QRTZ_ diff --git a/examples/src/main/resources/org/quartz/examples/example13/instance2.properties b/examples/src/main/resources/org/quartz/examples/example13/instance2.properties index 1eb90e348..2a459f327 100644 --- a/examples/src/main/resources/org/quartz/examples/example13/instance2.properties +++ b/examples/src/main/resources/org/quartz/examples/example13/instance2.properties @@ -22,6 +22,13 @@ org.quartz.jobStore.misfireThreshold: 60000 org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate + +# Optional: restrict which classes JDBCJobStore will deserialize from the database +# (job data maps, triggers, calendars) using a JEP-290 look-ahead ObjectInputFilter. +# It is off by default; when enabled, only classes matching the pattern are allowed. +# Tailor the allow-list to the classes your jobs actually persist, then reject the +# rest with "!*". The value uses java.io.ObjectInputFilter pattern syntax, e.g.: +#org.quartz.jobStore.driverDelegateInitString=objectInputFilter=java.lang.*;java.util.*;org.quartz.**;com.myapp.jobs.**;!* org.quartz.jobStore.useProperties=false org.quartz.jobStore.dataSource=myDS org.quartz.jobStore.tablePrefix=QRTZ_ diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CUBRIDDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CUBRIDDelegate.java index b94318471..12f99dcc4 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CUBRIDDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CUBRIDDelegate.java @@ -58,9 +58,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) if (bytes != null && bytes.length != 0) { binaryInput = new ByteArrayInputStream(bytes); - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } return obj; diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CacheDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CacheDelegate.java index 0532c9cb8..d4216e54a 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CacheDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/CacheDelegate.java @@ -22,7 +22,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.Blob; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -80,9 +79,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) throws ClassNot } else if (binaryInput instanceof ByteArrayInputStream && ((ByteArrayInputStream) binaryInput).available() == 0 ) { return null; } else { - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - return in.readObject(); - } + return readObjectFromBinaryStream(binaryInput); } } } finally { diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/GaussDBDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/GaussDBDelegate.java index 98829a27a..b4b734592 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/GaussDBDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/GaussDBDelegate.java @@ -21,7 +21,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.ResultSet; import java.sql.SQLException; @@ -68,9 +67,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) if(bytes != null && bytes.length != 0) { binaryInput = new ByteArrayInputStream(bytes); - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/HSQLDBDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/HSQLDBDelegate.java index e665c13a0..f06e23dc3 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/HSQLDBDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/HSQLDBDelegate.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.ResultSet; import java.sql.SQLException; @@ -67,9 +66,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) Object obj; - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); return obj; } diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java index 559d339c5..e699e8200 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/MSSQLDelegate.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; @@ -74,9 +73,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) Object obj; - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); return obj; } diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PointbaseDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PointbaseDelegate.java index 66edb6739..331d89c73 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PointbaseDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PointbaseDelegate.java @@ -22,7 +22,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.math.BigDecimal; import java.sql.Connection; import java.sql.PreparedStatement; @@ -410,9 +409,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) InputStream binaryInput = new ByteArrayInputStream(binaryData); if (binaryInput.available() != 0) { - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } return obj; diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PostgreSQLDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PostgreSQLDelegate.java index f0197c85c..3cfbbf3b0 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PostgreSQLDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/PostgreSQLDelegate.java @@ -21,7 +21,6 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.ResultSet; import java.sql.SQLException; @@ -66,9 +65,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) if(bytes != null && bytes.length != 0) { binaryInput = new ByteArrayInputStream(bytes); - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegate.java index a4ceb12a1..fe62b4fad 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegate.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.NotSerializableException; +import java.io.ObjectInputFilter; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.math.BigDecimal; @@ -96,7 +97,16 @@ public class StdJDBCDelegate implements DriverDelegate, StdJDBCConstants { protected String schedName; protected boolean useProperties; - + + /** + * Optional look-ahead deserialization filter (JEP 290) applied to every object + * read back from a JDBCJobStore BLOB (job data map, trigger, calendar). Configured + * via the delegate init-string setting {@code objectInputFilter=} (see + * {@link #initialize}). When {@code null} (the default), deserialization behavior + * is unchanged. + */ + protected ObjectInputFilter objectInputFilter; + protected ClassLoadHelper classLoadHelper; protected final List triggerPersistenceDelegates = new LinkedList<>(); @@ -148,7 +158,7 @@ public void initialize(Logger logger, String tablePrefix, String schedName, Stri String[] settings = initString.split("\\|"); for(String setting: settings) { - String[] parts = setting.split("="); + String[] parts = setting.split("=", 2); String name = parts[0]; if(parts.length == 1 || parts[1] == null || parts[1].isEmpty()) continue; @@ -165,6 +175,9 @@ public void initialize(Logger logger, String tablePrefix, String schedName, Stri } } } + else if(name.equals("objectInputFilter")) { + this.objectInputFilter = ObjectInputFilter.Config.createFilter(parts[1]); + } else throw new NoSuchDelegateException("Unknown setting: '" + name + "'"); } @@ -3420,9 +3433,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) && ((ByteArrayInputStream) binaryInput).available() == 0 ) { //do nothing } else { - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } } @@ -3430,6 +3441,33 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) return obj; } + /** + * Deserialize an object from the given binary stream, applying the configured + * {@link ObjectInputFilter} (if any) as a look-ahead deserialization guard + * (JEP 290). This is the single choke point through which every JDBCJobStore + * BLOB (job data map, trigger, calendar) is deserialized, so that a filter can + * be enforced consistently across the base delegate and every database-specific + * subclass rather than being duplicated (or omitted) per delegate. When no + * filter is configured, behavior is identical to a plain {@code ObjectInputStream}. + * + * @param binaryInput + * the stream containing the serialized object + * @return the deserialized Object + * @throws ClassNotFoundException + * if a class found during deserialization cannot be found + * @throws IOException + * if deserialization causes an error (including rejection by the filter) + */ + protected Object readObjectFromBinaryStream(InputStream binaryInput) + throws ClassNotFoundException, IOException { + try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { + if (objectInputFilter != null) { + in.setObjectInputFilter(objectInputFilter); + } + return in.readObject(); + } + } + /** *

* This method should be overridden by any delegate subclasses that need diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/SybaseDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/SybaseDelegate.java index 80428aafd..fbc5023c9 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/SybaseDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/SybaseDelegate.java @@ -21,7 +21,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -69,9 +68,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) Object obj; - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); return obj; } diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/WebLogicDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/WebLogicDelegate.java index fd6e05b61..76c7f3413 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/WebLogicDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/WebLogicDelegate.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.sql.Blob; import java.sql.ResultSet; import java.sql.SQLException; @@ -72,9 +71,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) } if (null != binaryInput) { - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } return obj; diff --git a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/oracle/OracleDelegate.java b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/oracle/OracleDelegate.java index 745187547..e57bae5ec 100644 --- a/quartz/src/main/java/org/quartz/impl/jdbcjobstore/oracle/OracleDelegate.java +++ b/quartz/src/main/java/org/quartz/impl/jdbcjobstore/oracle/OracleDelegate.java @@ -21,7 +21,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.ObjectInputStream; import java.math.BigDecimal; import java.sql.Blob; import java.sql.Connection; @@ -135,9 +134,7 @@ protected Object getObjectFromBlob(ResultSet rs, String colName) Object obj = null; InputStream binaryInput = rs.getBinaryStream(colName); if (binaryInput != null) { - try (ObjectInputStream in = new ObjectInputStream(binaryInput)) { - obj = in.readObject(); - } + obj = readObjectFromBinaryStream(binaryInput); } return obj; diff --git a/quartz/src/test/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegateTest.java b/quartz/src/test/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegateTest.java index 3d4102c12..2a8522f5b 100644 --- a/quartz/src/test/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegateTest.java +++ b/quartz/src/test/java/org/quartz/impl/jdbcjobstore/StdJDBCDelegateTest.java @@ -27,8 +27,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InvalidClassException; import java.io.NotSerializableException; +import java.io.ObjectOutputStream; +import java.util.HashMap; +import java.util.Map; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -72,6 +78,49 @@ void testSerializeJobData() throws IOException, NoSuchDelegateException { } } + @Test + void testReadObjectFromBinaryStreamWithoutFilterIsBehaviorPreserving() throws Exception { + StdJDBCDelegate delegate = new StdJDBCDelegate(); + delegate.initialize(LoggerFactory.getLogger(getClass()), "QRTZ_", "TESTSCHED", "INSTANCE", new SimpleClassLoadHelper(), false, ""); + + Map data = new HashMap<>(); + data.put("key", "value"); + data.put("count", 42); + + Object read = delegate.readObjectFromBinaryStream(new ByteArrayInputStream(serialize(data))); + assertEquals(data, read); + } + + @Test + void testReadObjectFromBinaryStreamAppliesConfiguredFilter() throws Exception { + StdJDBCDelegate delegate = new StdJDBCDelegate(); + // Restrict deserialization to the JDK types a job-data map round-trips, rejecting anything else. + delegate.initialize(LoggerFactory.getLogger(getClass()), "QRTZ_", "TESTSCHED", "INSTANCE", new SimpleClassLoadHelper(), false, + "objectInputFilter=java.util.*;java.lang.*;!*"); + + // A legitimate, allow-listed payload still deserializes unchanged. + Map data = new HashMap<>(); + data.put("key", "value"); + assertEquals(data, delegate.readObjectFromBinaryStream(new ByteArrayInputStream(serialize(data)))); + + // A class outside the allow-list is rejected by the filter before it is constructed. + byte[] disallowed = serialize(new UnexpectedType()); + assertThrows(InvalidClassException.class, + () -> delegate.readObjectFromBinaryStream(new ByteArrayInputStream(disallowed))); + } + + private static byte[] serialize(Object obj) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (ObjectOutputStream oos = new ObjectOutputStream(baos)) { + oos.writeObject(obj); + } + return baos.toByteArray(); + } + + private static class UnexpectedType implements java.io.Serializable { + private static final long serialVersionUID = 1L; + } + @Test void testSelectBlobTriggerWithNoBlobContent() throws JobPersistenceException, SQLException, IOException, ClassNotFoundException { StdJDBCDelegate jdbcDelegate = new StdJDBCDelegate();