Skip to content

Java: add extra sink for java/unsafe-deserialization #20025

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

Merged
merged 4 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The qualifiers of a calls to `readObject` on any classes that implement `java.io.ObjectInput` are now recognised as sinks for `java/unsafe-deserialization`. Previously this was only the case for classes which extend `java.io.ObjectInputStream`.
5 changes: 5 additions & 0 deletions java/ql/lib/semmle/code/java/JDK.qll
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ class TypeObjectOutputStream extends RefType {
TypeObjectOutputStream() { this.hasQualifiedName("java.io", "ObjectOutputStream") }
}

/** The type `java.io.ObjectInput`. */
class TypeObjectInput extends RefType {
TypeObjectInput() { this.hasQualifiedName("java.io", "ObjectInput") }
}

/** The type `java.io.ObjectInputStream`. */
class TypeObjectInputStream extends RefType {
TypeObjectInputStream() { this.hasQualifiedName("java.io", "ObjectInputStream") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ private import semmle.code.java.frameworks.google.Gson
private import semmle.code.java.frameworks.apache.Lang
private import semmle.code.java.Reflection

private class ObjectInputStreamReadObjectMethod extends Method {
ObjectInputStreamReadObjectMethod() {
private class ObjectInputReadObjectMethod extends Method {
ObjectInputReadObjectMethod() {
this.getDeclaringType().getASourceSupertype*() instanceof TypeObjectInput and
this.hasName("readObject")
}
}

private class ObjectInputStreamReadUnsharedMethod extends Method {
ObjectInputStreamReadUnsharedMethod() {
this.getDeclaringType().getASourceSupertype*() instanceof TypeObjectInputStream and
(this.hasName("readObject") or this.hasName("readUnshared"))
this.hasName("readUnshared")
}
}

Expand Down Expand Up @@ -147,12 +154,13 @@ private module SafeKryoFlow = DataFlow::Global<SafeKryoConfig>;
*/
predicate unsafeDeserialization(MethodCall ma, Expr sink) {
exists(Method m | m = ma.getMethod() |
m instanceof ObjectInputStreamReadObjectMethod and
m instanceof ObjectInputReadObjectMethod and
sink = ma.getQualifier() and
not exists(DataFlow::ExprNode node |
node.getExpr() = sink and
node.getTypeBound() instanceof SafeObjectInputStreamType
)
not DataFlow::exprNode(sink).getTypeBound() instanceof SafeObjectInputStreamType
or
m instanceof ObjectInputStreamReadUnsharedMethod and
sink = ma.getQualifier() and
not DataFlow::exprNode(sink).getTypeBound() instanceof SafeObjectInputStreamType
or
m instanceof XmlDecoderReadObjectMethod and
sink = ma.getQualifier()
Expand Down
29 changes: 23 additions & 6 deletions java/ql/test/query-tests/security/CWE-502/A.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package unsafedeserialization;

import java.io.*;
import java.net.Socket;
import java.beans.XMLDecoder;
import com.example.MyObjectInput;
import com.thoughtworks.xstream.XStream;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
Expand All @@ -10,13 +13,23 @@
import org.nibblesec.tools.SerialKiller;

public class A {
public Object deserialize1(Socket sock) throws java.io.IOException, ClassNotFoundException {
public Object deserialize1a(Socket sock) throws java.io.IOException, ClassNotFoundException {
InputStream inputStream = sock.getInputStream(); // $ Source
ObjectInputStream in = new ObjectInputStream(inputStream);
return in.readObject(); // $ Alert
}

public Object deserialize2(Socket sock) throws java.io.IOException, ClassNotFoundException {
public Object deserialize2() throws java.io.IOException, ClassNotFoundException {
ObjectInput objectInput = A.getTaintedObjectInput(); // $ Source
return objectInput.readObject(); // $ Alert
}

public Object deserialize3() throws java.io.IOException, ClassNotFoundException {
MyObjectInput objectInput = A.getTaintedMyObjectInput(); // $ Source
return objectInput.readObject(); // $ Alert
}

public Object deserialize4(Socket sock) throws java.io.IOException, ClassNotFoundException {
InputStream inputStream = sock.getInputStream(); // $ Source
ObjectInputStream in = new ObjectInputStream(inputStream);
return in.readUnshared(); // $ Alert
Expand All @@ -28,20 +41,20 @@ public Object deserializeWithSerialKiller(Socket sock) throws java.io.IOExceptio
return in.readUnshared(); // OK
}

public Object deserialize3(Socket sock) throws java.io.IOException {
public Object deserialize5(Socket sock) throws java.io.IOException {
InputStream inputStream = sock.getInputStream(); // $ Source
XMLDecoder d = new XMLDecoder(inputStream);
return d.readObject(); // $ Alert
}

public Object deserialize4(Socket sock) throws java.io.IOException {
public Object deserialize6(Socket sock) throws java.io.IOException {
XStream xs = new XStream();
InputStream inputStream = sock.getInputStream(); // $ Source
Reader reader = new InputStreamReader(inputStream);
return xs.fromXML(reader); // $ Alert
}

public void deserialize5(Socket sock) throws java.io.IOException {
public void deserialize7(Socket sock) throws java.io.IOException {
Kryo kryo = new Kryo();
Input input = new Input(sock.getInputStream()); // $ Source
A a1 = kryo.readObject(input, A.class); // $ Alert
Expand All @@ -56,7 +69,7 @@ private Kryo getSafeKryo() throws java.io.IOException {
return kryo;
}

public void deserialize6(Socket sock) throws java.io.IOException {
public void deserialize8(Socket sock) throws java.io.IOException {
Kryo kryo = getSafeKryo();
Input input = new Input(sock.getInputStream());
Object o = kryo.readClassAndObject(input); // OK
Expand Down Expand Up @@ -101,4 +114,8 @@ public void deserializeSnakeYaml4(Socket sock) throws java.io.IOException {
A o4 = yaml.loadAs(input, A.class); // $ Alert
A o5 = yaml.loadAs(new InputStreamReader(input), A.class); // $ Alert
}

static ObjectInput getTaintedObjectInput() { return null; }

static MyObjectInput getTaintedMyObjectInput() { return null; }
}
Loading