Harden XML parsing, serialization, and randomness (#3075)

This commit introduces several security hardening improvements across the codebase:
1. XML Processing: Hardened `TransformerFactory` and `SchemaFactory` instantiations in `EppMessage.java` by explicitly enabling `XMLConstants.FEATURE_SECURE_PROCESSING` and disabling external schema access.
2. Randomness: Replaced instances of `java.util.Random` with `java.security.SecureRandom` in `SelfSignedCaCertificate.java` for stronger entropy. (Added documentation in `ProxyModule.java` explaining why `java.util.Random` is intentionally retained there for metrics sampling).
3. Deserialization: Hardened `SerializeUtils.java` by injecting an `ObjectInputFilter` into the `ObjectInputStream`, restricting deserialization strictly to expected `google.registry` classes and standard Java collections.
This commit is contained in:
Ben McIlwain
2026-06-01 14:25:42 +00:00
committed by GitHub
parent c5abd2a7c9
commit 0030645b1a
7 changed files with 60 additions and 5 deletions
@@ -27,7 +27,6 @@ import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.Date;
import java.util.Random;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
@@ -45,7 +44,7 @@ public class SelfSignedCaCertificate {
private static final String DEFAULT_ISSUER_FQDN = "registry-test";
private static final Random RANDOM = new Random();
private static final SecureRandom RANDOM = new SecureRandom();
private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
private static final KeyPairGenerator keyGen = createKeyPairGenerator();
private static final ImmutableMap<String, String> KEY_SIGNATURE_ALGS =
@@ -20,6 +20,7 @@ import static com.google.common.io.BaseEncoding.base16;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputFilter.Config;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
@@ -60,7 +61,13 @@ public final class SerializeUtils {
return null;
}
try {
return type.cast(new ObjectInputStream(new ByteArrayInputStream(objectBytes)).readObject());
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(objectBytes));
// Restrict deserialization to known, trusted packages to prevent RCE gadget chain attacks.
ois.setObjectInputFilter(
Config.createFilter(
"google.registry.**;com.google.common.**;java.**;javax.**;jakarta.**;"
+ "org.hibernate.**;org.joda.**;com.googlecode.objectify.**;!*"));
return type.cast(ois.readObject());
} catch (ClassNotFoundException | IOException e) {
throw new IllegalArgumentException(
"Unable to deserialize: objectBytes=" + base16().encode(objectBytes), e);
@@ -20,9 +20,12 @@ import static google.registry.util.SerializeUtils.parse;
import static google.registry.util.SerializeUtils.serialize;
import static google.registry.util.SerializeUtils.stringify;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.InvalidClassException;
import java.io.Serializable;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
/** Unit tests for {@link SerializeUtils}. */
class SerializeUtilsTest {
@@ -111,4 +114,19 @@ class SerializeUtilsTest {
assertThrows(NullPointerException.class, () -> parse(String.class, null));
assertThat(thrown).hasMessageThat().contains("Object string cannot be null");
}
@Test
void testDeserialize_unauthorizedClass_isRejectedByFilter() {
// AssertionFailedError implements Serializable but is NOT in the
// whitelist of allowed deserialization packages.
AssertionFailedError untrustedObject = new AssertionFailedError("test");
try {
deserialize(Object.class, serialize(untrustedObject));
fail("Expected an exception");
} catch (IllegalArgumentException e) {
assertThat(e).hasCauseThat().isInstanceOf(InvalidClassException.class);
assertThat(e).hasCauseThat().hasMessageThat().contains("REJECTED");
}
}
}