HardenedObjectInputStreamTest.java
/*
* Logback: the reliable, generic, fast and flexible logging framework.
* Copyright (C) 1999-2026, QOS.ch. All rights reserved.
*
* This program and the accompanying materials are dual-licensed under
* either the terms of the Eclipse Public License v2.0 as published by
* the Eclipse Foundation
*
* or (per the licensee's choosing)
*
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
*/
package ch.qos.logback.core.net;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashSet;
import java.util.Set;
import ch.qos.logback.core.Context;
import ch.qos.logback.core.ContextBase;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class HardenedObjectInputStreamTest {
ByteArrayOutputStream bos;
ObjectOutputStream oos;
HardenedObjectInputStream inputStream;
Context context = new ContextBase();
String[] whitelist = new String[] { Innocent.class.getName() };
@BeforeEach
public void setUp() throws Exception {
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
}
@AfterEach
public void tearDown() throws Exception {
}
@Test
public void smoke() throws ClassNotFoundException, IOException {
Innocent innocent = new Innocent();
innocent.setAnInt(1);
innocent.setAnInteger(2);
innocent.setaString("smoke");
Innocent back = writeAndRead(innocent);
assertEquals(innocent, back);
}
private Innocent writeAndRead(Innocent innocent) throws IOException, ClassNotFoundException {
writeObject(oos, innocent);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
inputStream = new HardenedObjectInputStream(context, bis, whitelist);
Innocent fooBack = (Innocent) inputStream.readObject();
inputStream.close();
return fooBack;
}
private void writeObject(ObjectOutputStream oos, Object o) throws IOException {
oos.writeObject(o);
oos.flush();
oos.close();
}
@Test
public void denialOfService() throws ClassNotFoundException, IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(payload());
inputStream = new HardenedObjectInputStream(context, bis, whitelist);
try {
assertThrows(InvalidClassException.class, () -> inputStream.readObject());
} finally {
inputStream.close();
}
}
private byte[] payload() throws IOException {
Set root = buildEvilHashset();
writeObject(oos, root);
return bos.toByteArray();
}
private Set buildEvilHashset() {
Set root = new HashSet();
Set s1 = root;
Set s2 = new HashSet();
for (int i = 0; i < 100; i++) {
Set t1 = new HashSet();
Set t2 = new HashSet();
t1.add("foo"); // make it not equal to t2
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
return root;
}
/**
* Demonstrates that HardenedObjectInputStream.resolveProxyClass works correctly
* by rejecting deserialization of dynamic proxy classes, even when the interfaces
* they implement are whitelisted.
*/
@Test
public void resolveProxyClassRejectsDynamicProxies() throws Exception {
ProxyInterface proxy = (ProxyInterface) Proxy.newProxyInstance(
getClass().getClassLoader(),
new Class<?>[]{ProxyInterface.class},
new TestInvocationHandler()
);
// Serialize the proxy instance
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(proxy);
}
// Attempt to deserialize using HardenedObjectInputStream.
// We deliberately whitelist both the interface and the invocation handler.
// Despite this, deserialization must fail because resolveProxyClass always
// throws InvalidClassException for proxy classes.
String[] whitelist = new String[]{
ProxyInterface.class.getName(),
TestInvocationHandler.class.getName()
};
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
HardenedObjectInputStream hardenedOis = new HardenedObjectInputStream(context, bis, whitelist);
assertThrows(InvalidClassException.class, hardenedOis::readObject);
hardenedOis.close();
}
/**
* A marker interface for the dynamic proxy used in the resolveProxyClass test.
*/
interface ProxyInterface extends Serializable {
String getMessage();
}
/**
* A serializable InvocationHandler used to create the test dynamic proxy.
*/
static class TestInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 1L;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getMessage".equals(method.getName())) {
return "hello from proxy";
}
return null;
}
}
}