You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
JDBCJobStore deserializes the job-data-map, trigger and calendar BLOBs it reads back from the database with a raw new ObjectInputStream(...).readObject(), with no look-ahead deserialization filter. The same unfiltered pattern is duplicated across the base delegate and every database-specific delegate, so there is currently no single place where a filter could be enforced (CWE-502 / JEP-290 gap).
The read sites (all on the current main):
StdJDBCDelegate.getObjectFromBlob(), the base implementation, reached for COL_JOB_DATAMAP (selectJobDetail), COL_BLOB (selectTrigger) and COL_CALENDAR (selectCalendar).
The per-dialect overrides that duplicate the same new ObjectInputStream(binaryInput).readObject(): PostgreSQLDelegate, MSSQLDelegate, OracleDelegate, HSQLDBDelegate, SybaseDelegate, WebLogicDelegate, PointbaseDelegate, CUBRIDDelegate, CacheDelegate, GaussDBDelegate (the last added recently via Support Gauss DB #1355, i.e. new delegates keep copying the pattern).
Why
Java object deserialization of data that may be influenced by an attacker is the classic gadget-chain risk. For the JDBC job store this is defense-in-depth (it presumes influence over the stored BLOBs, e.g. via a shared/compromised job-store database), but there is currently no way to constrain it at all short of a JVM-wide jdk.serialFilter. Since Java 9 (ObjectInputFilter, this project targets Java 11) a per-store filter is a standard, low-cost hardening, and having one central choke point is the prerequisite for it.
Proposal
Centralize the deserialization into a single protected helper, StdJDBCDelegate.readObjectFromBinaryStream(InputStream), that every delegate calls instead of duplicating new ObjectInputStream(...).readObject(). The dialect delegates keep their own dialect-specific stream-fetching; only the readObject step is shared.
Support an optional ObjectInputFilter on the delegate, configured through the existing delegate init-string as objectInputFilter=<JEP-290 pattern> (e.g. org.quartz.jobStore.driverDelegateInitString = objectInputFilter=maxdepth=20;java.**;org.quartz.**;!*). When unset, behavior is identical to today.
This also gives a natural home for the class-loading fix proposed in #1153 (ClassLoaderObjectInputStream), which touched exactly these delegates but was closed only for procedural reasons (ownership change / re-signing); both concerns can live behind the one helper.
Open question (the reason I'm opening an issue rather than just a PR)
What should the default be? Because a job-data map serialized with useProperties=false can legitimately contain arbitrary application classes, a strict default allow-list would break existing users, so the draft below defaults the filter to null (no change) and leaves it opt-in. Options, happy to implement whichever you prefer:
(a) keep it opt-in (draft as-is), zero behavior change;
(b) apply a conservative, non-breaking resource-limit default (e.g. a generous maxdepth/maxrefs) that never rejects legitimate object graphs but stops depth/reference bombs, still fully overridable;
(c) a documented default reject-list of the well-known gadget packages.
Draft PR
I've opened a draft PR implementing option (a): #1494. It compiles on the Java 11 toolchain and StdJDBCDelegateTest passes (7/7), including two added tests proving (1) no filter = unchanged round-trip and (2) a configured filter rejects a non-allow-listed class while still deserializing legitimate data. Commits are DCO signed-off. I'll adjust the default policy to whatever the team prefers before it leaves draft.
Summary
JDBCJobStoredeserializes the job-data-map, trigger and calendar BLOBs it reads back from the database with a rawnew ObjectInputStream(...).readObject(), with no look-ahead deserialization filter. The same unfiltered pattern is duplicated across the base delegate and every database-specific delegate, so there is currently no single place where a filter could be enforced (CWE-502 / JEP-290 gap).The read sites (all on the current
main):StdJDBCDelegate.getObjectFromBlob(), the base implementation, reached forCOL_JOB_DATAMAP(selectJobDetail),COL_BLOB(selectTrigger) andCOL_CALENDAR(selectCalendar).new ObjectInputStream(binaryInput).readObject():PostgreSQLDelegate,MSSQLDelegate,OracleDelegate,HSQLDBDelegate,SybaseDelegate,WebLogicDelegate,PointbaseDelegate,CUBRIDDelegate,CacheDelegate,GaussDBDelegate(the last added recently via Support Gauss DB #1355, i.e. new delegates keep copying the pattern).Why
Java object deserialization of data that may be influenced by an attacker is the classic gadget-chain risk. For the JDBC job store this is defense-in-depth (it presumes influence over the stored BLOBs, e.g. via a shared/compromised job-store database), but there is currently no way to constrain it at all short of a JVM-wide
jdk.serialFilter. Since Java 9 (ObjectInputFilter, this project targets Java 11) a per-store filter is a standard, low-cost hardening, and having one central choke point is the prerequisite for it.Proposal
StdJDBCDelegate.readObjectFromBinaryStream(InputStream), that every delegate calls instead of duplicatingnew ObjectInputStream(...).readObject(). The dialect delegates keep their own dialect-specific stream-fetching; only thereadObjectstep is shared.ObjectInputFilteron the delegate, configured through the existing delegate init-string asobjectInputFilter=<JEP-290 pattern>(e.g.org.quartz.jobStore.driverDelegateInitString = objectInputFilter=maxdepth=20;java.**;org.quartz.**;!*). When unset, behavior is identical to today.This also gives a natural home for the class-loading fix proposed in #1153 (
ClassLoaderObjectInputStream), which touched exactly these delegates but was closed only for procedural reasons (ownership change / re-signing); both concerns can live behind the one helper.Open question (the reason I'm opening an issue rather than just a PR)
What should the default be? Because a job-data map serialized with
useProperties=falsecan legitimately contain arbitrary application classes, a strict default allow-list would break existing users, so the draft below defaults the filter to null (no change) and leaves it opt-in. Options, happy to implement whichever you prefer:maxdepth/maxrefs) that never rejects legitimate object graphs but stops depth/reference bombs, still fully overridable;Draft PR
I've opened a draft PR implementing option (a): #1494. It compiles on the Java 11 toolchain and
StdJDBCDelegateTestpasses (7/7), including two added tests proving (1) no filter = unchanged round-trip and (2) a configured filter rejects a non-allow-listed class while still deserializing legitimate data. Commits are DCO signed-off. I'll adjust the default policy to whatever the team prefers before it leaves draft.