Vulnerability Management

Dataflow Analysis in Modern Codebases

Dataflow analysis is the workhorse behind most vulnerability research. Here's how it adapts to TypeScript, Rust, and the polyglot realities of modern software.

Nayan Dey
Senior Security Engineer
8 min read

The workflow of vulnerability research has not changed much in twenty years, but the codebases have. A typical modern application is a TypeScript frontend talking to a Go microservice that calls a Rust compute kernel and persists state through a Python data pipeline. The dataflow analysis techniques that worked well for a single Java monolith in 2005 do not compose cleanly across this stack, and security teams spend a lot of effort figuring out what does.

This post is about what has changed in dataflow analysis since the Livshits and Lam era, and what a practitioner needs to know to do useful work in a modern codebase. It is not a textbook introduction. It is a working practitioner's view of what techniques are doing real work in 2024.

What Dataflow Analysis Actually Does

At its core, dataflow analysis answers questions of the form: "for every point in the program, what do we know about the values of the variables there?" The classic applications are reaching definitions, live variables, and available expressions, which are the bread and butter of optimizing compilers.

For security work, the dataflow questions are usually framed as reachability: "can data from point A reach point B, possibly through a series of transformations?" This is the core of taint analysis, but it also underlies most other security analyses including deserialization gadget search, cryptographic misuse detection, and authorization bypass discovery.

The lattice-theoretic framework from Kildall's 1973 paper, "A Unified Approach to Global Program Optimization," is still the mathematical foundation. Every modern dataflow analyzer is some variation of Kildall's iterative fixpoint algorithm, specialized to a particular lattice and transfer function.

The Shift to Interprocedural Everything

The big shift in the last ten years is that almost all interesting security bugs cross function boundaries. Intraprocedural analyses catch toy examples, but real codebases distribute security-sensitive operations across many files, classes, and services.

Interprocedural analysis comes in several flavors. The oldest is call-string-based context sensitivity, where the analyzer tracks the sequence of calls that led to the current function. This is precise but expensive.

Functional summaries are a more scalable approach. For each function, the analyzer computes a summary of the function's dataflow behavior (what taint it introduces, what taint it clears, what taint it passes through) and uses those summaries when analyzing callers. The summaries can be computed once and reused across all callers, which gives a linear rather than exponential blowup.

The IFDS framework, from Thomas Reps, Susan Horwitz, and Mooly Sagiv (1995 POPL), is the canonical formulation of interprocedural distributive analysis. Modern engines like Soot's FlowDroid and Facebook's Infer descend directly from IFDS. The 2014 PLDI paper, "Boomerang: Demand-Driven Flow- and Context-Sensitive Pointer Analysis for Java," by Johannes Späth and collaborators, is a good modern reference.

Framework Modeling

A JavaScript analyzer that does not understand Express routes, middleware chains, and async middleware is not actually analyzing JavaScript web applications. A Java analyzer that does not model Spring's dependency injection, annotation-based request binding, and JPA entity lifecycle is not analyzing real Spring applications.

This is the framework-modeling problem, and it consumes an enormous fraction of the engineering budget of every commercial static analyzer. The choice of which frameworks to model, at what level of fidelity, is one of the main ways analyzers compete.

CodeQL's library of framework models is the most extensive in the industry. The Semgrep team has been catching up quickly, and their Semgrep Pro offering now ships with explicit support for Django, FastAPI, Express, Spring, Laravel, and a growing list of others. The key capability is recognizing framework-specific patterns for routing, request binding, and response rendering, so that the analyzer can mark the right things as sources and sinks.

For proprietary or less-common frameworks, the practical answer is to write your own framework models. Both CodeQL and Semgrep support this. The investment pays off quickly because the false-positive rate on custom-framework code drops dramatically once the analyzer understands the framework idioms.

TypeScript, Dynamic Types, and Duck-Typed Dataflow

TypeScript is a special case worth its own section. The type system helps the analyzer narrow the set of concrete types at each program point, but the underlying JavaScript runtime still does duck typing, which means dataflow can reach places the types would not suggest.

The 2018 PLDI paper, "TAJS: Type Analysis for JavaScript," by Simon Holm Jensen and colleagues, laid the groundwork. Modern engines like CodeQL's JavaScript support extend TAJS-style type inference with flow-sensitive refinement. The result is workable but imperfect. A TypeScript codebase with aggressive use of any or runtime type assertions will degrade the analysis.

The practical advice for TypeScript codebases is to keep type coverage high and to avoid any in security-sensitive paths. The analyzer gets markedly better results on well-typed code, and the same discipline that helps the analyzer also helps developers.

Rust and the Ownership Model

Rust is the opposite of TypeScript in terms of static analysis friendliness. The borrow checker provides strong guarantees about aliasing, which means a Rust-aware analyzer can make much sharper claims about dataflow than it could for C or C++.

The Prusti project, from ETH Zurich, uses Rust's ownership information to do heavy-weight verification of correctness properties. The Miri interpreter catches undefined behavior at runtime, which complements static analysis. For security work, the most useful Rust tool is probably cargo-audit, which scans for known-vulnerable dependencies, combined with a general-purpose analyzer like Semgrep for pattern-based checks.

The open problem in Rust static analysis is unsafe code. Once you enter an unsafe block, the guarantees disappear and you are effectively back to C-level analysis. The recent Project Oak work at Google on verifying unsafe Rust is pushing the frontier here.

Cross-Language Dataflow

The hardest problem in modern codebases is dataflow that crosses language boundaries. When a TypeScript frontend POSTs to a Go backend, there is a real dataflow from the frontend input to the backend processing, but no single analyzer sees both sides.

A few approaches exist. The first is schema-based reasoning: if both sides use a shared protobuf, OpenAPI, or GraphQL schema, the analyzer can use the schema as a bridge. The 2022 ICSE paper, "Cross-language Taint Analysis for Android Applications," by researchers at Fraunhofer IEM, formalized this for Android's JNI boundary and the ideas generalize.

The second is trace-based analysis, where the analyzer instruments the application at runtime and records cross-boundary dataflow. This is closer to IAST (interactive application security testing) than pure static analysis, but it fills the gap that pure SAST cannot.

The third is whole-system modeling, where each language's analysis emits a common intermediate representation and a meta-analyzer combines them. This is the approach taken by some academic tools but has not seen much commercial adoption.

For most teams, the practical compromise is to analyze each component separately, pay close attention to the trust boundaries between them, and audit the serialization and deserialization code at each boundary with extra care.

Demand-Driven Analysis

A relatively recent development is demand-driven dataflow analysis, where the analyzer computes facts only at the points the user queries. This is a major efficiency win for interactive use cases, where a researcher asks a specific question about a specific function rather than running a whole-program analysis.

The Boomerang paper mentioned earlier is the main academic reference. In industry, IDE plugins for CodeQL and Semgrep use demand-driven techniques to give fast feedback on queries written in the editor. For a researcher exploring a large codebase, the experience is closer to interactive debugging than batch analysis.

Scaling to Millions of Lines

At the monorepo scale, even well-engineered dataflow analyses struggle. Google's Tricorder, Meta's Infer, and Microsoft's PROSE each approach this differently. The common themes are incremental analysis (re-analyze only what changed), aggressive summarization, and tight integration with code review so that findings are surfaced at the moment developers can act on them.

For teams approaching the megaline scale, the tooling choice matters less than the deployment discipline. Any analysis is useful if it runs on every PR and surfaces findings to the right developer. No analysis is useful if it runs once a quarter and produces a PDF that nobody reads.

How Safeguard Helps

Safeguard runs interprocedural, framework-aware dataflow analysis across every language in your stack and stitches the results together at schema boundaries to produce cross-language reachability findings. When a CVE drops in a TypeScript library used by your frontend, Safeguard can tell you whether the vulnerable function is reachable from any HTTP endpoint your backend exposes. The demand-driven query interface lets your researchers ask targeted questions about specific functions and get answers in seconds rather than waiting for a whole-program analysis. All of this runs continuously in CI, so findings appear when a pull request is opened rather than weeks after merge.

Never miss an update

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