Repository navigation
fix(rocksdb): filter native deserialization of DBValue/DBNull index keys #1346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.evil; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.ObjectInputStream; | ||
| import java.io.Serializable; | ||
|
|
||
| /** Test gadget whose readObject records that native deserialization ran. */ | ||
| public class SerialGadget implements Serializable, Comparable<SerialGadget> { | ||
| public static volatile boolean deserialized; | ||
|
|
||
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { | ||
| in.defaultReadObject(); | ||
| deserialized = true; | ||
| } | ||
|
|
||
| @Override | ||
| public int compareTo(SerialGadget o) { | ||
| return 0; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package org.dizitart.no2.rocksdb.formatter; | ||
|
|
||
| import com.evil.SerialGadget; | ||
| import org.dizitart.no2.common.DBNull; | ||
| import org.dizitart.no2.common.DBValue; | ||
| import org.dizitart.no2.exceptions.NitriteIOException; | ||
| import org.junit.Test; | ||
|
|
||
| import static org.junit.Assert.*; | ||
|
|
||
| /** | ||
| * GHSA-463g-rx95-c9fp: DBValue/DBNull index keys must not natively deserialize unlisted classes. | ||
| */ | ||
| public class DBValueSerializerFilterTest { | ||
|
|
||
| @Test | ||
| public void testGadgetInIndexKeyIsRejected() { | ||
| KryoObjectFormatter formatter = new KryoObjectFormatter(); | ||
| byte[] bytes = formatter.encodeKey(new DBValue(new SerialGadget())); | ||
|
|
||
| SerialGadget.deserialized = false; | ||
| try { | ||
| formatter.decodeKey(bytes, DBValue.class); | ||
| fail("unlisted class should be rejected"); | ||
| } catch (NitriteIOException expected) { | ||
| // rejected before readObject ran | ||
| } | ||
| assertFalse(SerialGadget.deserialized); | ||
| } | ||
|
|
||
| @Test | ||
| public void testLegitKeysRoundTrip() { | ||
| KryoObjectFormatter formatter = new KryoObjectFormatter(); | ||
| DBValue value = new DBValue("abc"); | ||
| assertEquals(value, formatter.decodeKey(formatter.encodeKey(value), DBValue.class)); | ||
| DBNull nul = DBNull.getInstance(); | ||
| assertNotNull(formatter.decodeKey(formatter.encodeKey(nul), DBNull.class)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package org.dizitart.no2.common.util; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.file.Files; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Paths; | ||
| import java.util.*; | ||
| import java.util.regex.Pattern; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import static org.junit.Assert.assertTrue; | ||
|
|
||
| /** | ||
| * CWE-502 guard (GHSA-9297-g93h-86gg, GHSA-7w7v-2j76-mqwp, GHSA-q86x-r2q3-vf4p, | ||
| * GHSA-463g-rx95-c9fp): every native java deserialization sink in main sources must | ||
| * go through an allowlist. A new sink fails this test until it is filtered | ||
| * (use {@link ObjectUtils#createFilteredInputStream}) and its file reviewed and listed below. | ||
| */ | ||
| public class DeserializationSinkGuardTest { | ||
| private static final Pattern SINK = Pattern.compile( | ||
| "new ObjectInputStream\\s*\\(|extends ObjectInputStream|\\bJavaSerializer\\b|\\bExternalizableSerializer\\b" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Detect equivalent deserialization constructor syntax. An unreviewed source file containing Recognize qualified names and whitespace variants, or inspect syntax tokens. Add regression cases for these forms. Based on learnings, source assertions must target the semantic root cause rather than one formatting variant.
|
||
| + "|XMLDecoder|readUnshared\\s*\\("); | ||
|
|
||
| // reviewed: each of these installs an allowlist, or only reads bytes it just wrote itself | ||
| private static final Set<String> REVIEWED = new HashSet<>(Arrays.asList( | ||
| "ObjectUtils.java", // createFilteredInputStream; deepCopy reads its own bytes | ||
| "FilteredObjectDataType.java", // mvstore, JEP 290 filter | ||
| "NitriteObjectInputStream.java", // legacy v1 migration, JEP 290 filter | ||
| "NitriteSerializers.java" // rocksdb FilteredJavaSerializer | ||
| )); | ||
|
|
||
| @Test | ||
| public void testNoUnreviewedDeserializationSink() throws IOException { | ||
| Path root = Paths.get("").toAbsolutePath().getParent(); | ||
| List<String> offenders; | ||
| try (Stream<Path> files = Files.walk(root)) { | ||
| offenders = files | ||
| .filter(p -> p.toString().endsWith(".java") || p.toString().endsWith(".kt")) | ||
| .filter(p -> p.toString().contains("/src/main/")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Select source directories without platform-specific separators. On Windows, Match path components instead of the rendered path string.
|
||
| .filter(p -> !REVIEWED.contains(p.getFileName().toString())) | ||
| .filter(DeserializationSinkGuardTest::hasSink) | ||
| .map(p -> root.relativize(p).toString()) | ||
| .collect(Collectors.toList()); | ||
| } | ||
| assertTrue("Unfiltered deserialization sink(s): " + offenders, offenders.isEmpty()); | ||
| } | ||
|
|
||
| private static boolean hasSink(Path path) { | ||
| try { | ||
| return SINK.matcher(new String(Files.readAllBytes(path))).find(); | ||
| } catch (IOException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Index keys that hold application
Comparablevalues can fail to read.Core indexes any
Comparablefield value.SingleFieldIndexwraps such a value inDBValue, andDBValueserializes it with Java serialization.FilteredJavaSerializer.readpassesnullas the allowlist, so onlyorg.dizitart.no2.**,java.**, andkotlin.collections.**classes can be deserialized. An application enum in an index key is rejected withInvalidClassException, which surfaces asNitriteIOException. The inheritedJavaSerializerread these keys without this check. Existing index entries that hold such values can therefore fail when they are read.The reader also ignores the
allowedClassesconfigured onKryoObjectFormatter. An application type that the document path allows is still rejected in an index key.Core does not reject these values before persistence.
IndexValidatorchecks onlyComparableassignability.Pass the formatter's configured allowlist into
FilteredJavaSerializerin place ofnull. Keep rejecting classes outside that allowlist. Add a round-trip test for aDBValuekey that contains an allowlisted application enum. Add a read test for a key written by the previous serializer.Source: Coding guidelines