Safeguard
Application Security

Bytecode vs. source analysis: static analysis techniques for Java and Kotlin

SpotBugs scans compiled .class files for 400+ bug patterns; Semgrep parses source directly. Neither alone would have caught CVE-2015-7501 fast enough.

Safeguard Research Team
Research
5 min read

On November 12, 2015, Apache Commons Collections shipped version 3.2.2 to close a gadget-chain deserialization flaw tracked as CVE-2015-7501 — a bug that let an attacker achieve remote code execution on IBM WebSphere, Oracle WebLogic, and JBoss simply by sending a crafted serialized object through JMX, RMI, or EJB interfaces. The flaw lived in InvokerTransformer and InstantiateTransformer, two classes that let readObject() invoke arbitrary methods reflectively, and it had shipped unnoticed in Commons Collections 3.0 through 3.2.1 and 4.0 for years. No source-level pattern matcher flagged it, because the dangerous behavior only existed once bytecode from multiple unrelated classes was chained together at deserialization time. That gap is exactly why JVM static analysis split into two genuinely different disciplines — bytecode analysis and source/AST analysis — and why mature Java and Kotlin security programs run both rather than picking one. This post walks through what each approach actually sees, where they diverge on Kotlin's compiled output, and which vulnerability classes belong to which.

What does bytecode analysis actually see that source analysis doesn't?

Bytecode analysis operates on compiled .class files rather than .java or .kt source, which means it sees the program the JVM will actually execute — including synthetic methods, compiler-inserted null checks, and the exact shape of control flow after optimization. SpotBugs, the actively maintained successor to FindBugs, ships more than 400 bug-pattern detectors built on this model and catches classes of defects that are difficult to see in source at all: double-checked locking without volatile, unreleased database connections and file handles, inconsistent synchronization on a field accessed from multiple threads, and equals/hashCode contract violations. Because Kotlin compiles down to standard JVM bytecode, SpotBugs can analyze Kotlin-derived .class files too, though its detectors were written against idiomatic Java patterns, so coverage of Kotlin-specific constructs like data classes and coroutines is partial. The tradeoff is that bytecode analysis needs a completed build — it cannot run against a source tree that doesn't compile.

What does a security-focused bytecode plugin add?

FindSecBugs, a plugin built on top of the SpotBugs engine, adds roughly 144 detectors aimed specifically at security defects rather than general code quality: SQL and LDAP injection, reflected and stored XSS, path traversal, XML external entity (XXE) processing, weak cryptographic algorithms (like DES or ECB-mode AES), hardcoded credentials, and — directly relevant to the Commons Collections case — insecure deserialization via ObjectInputStream.readObject(). FindSecBugs explicitly supports Kotlin, Groovy, and Scala in addition to Java, because all four compile to the same bytecode substrate SpotBugs consumes. The practical value is that a security team gets these checks without waiting for a source-based tool to add a new rule for every JVM language on top of Java — the plugin inherits multi-language coverage for free from the bytecode layer underneath it.

How does source/AST analysis like Semgrep complement that?

Source-based tools such as Semgrep parse .java and .kt files directly into an abstract syntax tree and build cross-file, cross-function dataflow graphs on top of it, which lets them trace taint from an untrusted source — a request parameter, a deserialized DTO field, an environment variable — through intermediate assignments to a dangerous sink like a JDBC Statement.execute() call or a Runtime.exec() invocation. Two properties make this the natural complement to bytecode scanning rather than a replacement: it works without a successful build, so it can run in a pre-merge CI check on a branch that doesn't compile yet, and it supports custom rule authoring in a lightweight pattern-matching syntax, so a team can write a rule the day a new internal API is introduced instead of waiting for a vendor to ship a detector. What source analysis structurally cannot see is the compiler-inserted behavior bytecode tools specialize in — synthetic bridge methods, autoboxing side effects, or JVM-level synchronization semantics — which is why the two approaches are described industry-wide as complementary layers rather than competing products.

What did CVE-2015-7501 actually teach the industry about insecure deserialization?

The Commons Collections gadget chain became the canonical teaching example for insecure deserialization (CWE-502) because it demonstrated that "dangerous" behavior can emerge from otherwise-benign library code with no vulnerable-looking line to flag. InvokerTransformer was designed to let a Transformer invoke an arbitrary method by reflection — a legitimate utility feature — and AnnotationInvocationHandler#readObject simply deserialized a Map that happened to contain one. Neither class contains an obviously dangerous call in isolation; the vulnerability only exists in the composition, reachable only through a JVM's native serialization mechanism at deserialization time. The fix in Commons Collections 3.2.2 and 4.1 disabled deserialization for the specific transformer classes rather than patching a single function. For static analysis, the lesson was concrete: pattern matchers that look for a single dangerous call miss composed vulnerabilities entirely, which is why bytecode-level detectors for readObject() overrides and taint-aware source scanners for object-graph reconstruction both became standard checks rather than one being sufficient on its own.

How does Safeguard apply this to Java and Kotlin specifically?

Safeguard's first-party SAST engine includes Java in its phase-1 language coverage alongside JavaScript/TypeScript and Python, tracing untrusted input from a source — a request parameter, CLI argument, or file read — to a dangerous sink such as a SQL query or command execution across functions and files, with each finding carrying the ordered dataflow trace and a CWE/OWASP mapping. On top of that, Safeguard's reachability engine treats the JVM as its own analysis tier: Java, Kotlin, and Scala have GA-level static and dynamic reachability, explicitly described as high-fidelity analysis of compiled code — meaning it builds call graphs from bytecode, not just source text, to determine whether a vulnerable dependency method (an InvokerTransformer-style gadget, for instance) is actually invoked by your application rather than merely present on the classpath. That combination is the practical answer to the bytecode-vs-source question: source-level SAST finds the taint path in your own code, and bytecode-based reachability confirms whether a flagged dependency vulnerability is real risk or dead weight in a JAR you never call into.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.