How the FLOWS_TO Taint Analysis Detects Security Vulnerabilities in Code Graph RAG

The FLOWS_TO taint analysis tracks untrusted data movement through code by emitting graph edges during intra-procedural walks, then uses BFS-based reachability queries to determine if vulnerabilities are actually exploitable in reachable execution paths.

The FLOWS_TO relationship is the core mechanism in the vitali87/code-graph-rag repository for modeling how sensitive data propagates through a codebase. By capturing taint flows as native graph edges, the system can precisely answer whether untrusted input from a source (such as environment variables) can reach a dangerous sink (such as network calls). This article explains the complete pipeline from flow generation to security verdicts.

Taint Flow Generation in the FlowProcessor

The FlowProcessor (codebase_rag/parsers/flow_access/processor.py) performs the intra-procedural analysis that populates FLOWS_TO relationships. It walks each function's control flow, tracks how tainted values move between variables, and defers edge emission until the fixpoint is reached.

Enabling and Configuring Flow Capture

Before processing begins, the processor verifies that the capture selection enables the FLOWS_TO relationship:

self._enabled = selection.rel_enabled(cs.RelationshipType.FLOWS_TO)

It then loads language-specific I/O sink definitions (IO_SINKS) that classify calls as read-sources (where untrusted data enters), write-sinks (where tainted data exiting is dangerous), or read-write sinks.

Intra-procedural Flow Walking

The processor selects a walking strategy based on the target language:

  • Lean flow (self._process_lean_flow) – Used for non-Python languages, performing basic taint tracking without branch sensitivity.
  • Path-sensitive MAY walk – Used for Python, evaluating each conditional branch separately and unioning taint states at merge points to balance precision and performance.

Seeding Taint and Emitting Edges

Taint analysis begins by seeding pseudo-origins for every function parameter:

tainted[pname] = Taint(..., {pname})

Free variables are similarly seeded so that taint reaching a sink becomes a capture summary. During the walk, the processor emits three categories of FLOWS_TO edges:

  1. Resource → Resource: Connects a read source to a write sink when tainted data flows directly between resources.
  2. Caller → Callee: Records argument passing via arg edges that link the caller's resource node to the callee's parameter.
  3. Callee → Caller: Records return values via return edges connecting the callee's resource back to the caller's assignment site.

All deferred edges materialize in the finalize() method once the per-function return-taint fixpoint is known.

Graph Schema and Storage

FLOWS_TO is defined as a relationship type in codebase_rag/constants/graph.py:

class RelationshipType(StrEnum):
    READS_FROM = "READS_FROM"
    WRITES_TO = "WRITES_TO"
    FLOWS_TO = "FLOWS_TO"

It belongs to the IO capture group, ensuring edges are stored only when the io capture is enabled in the selection configuration.

Querying Taint Reachability with FlowVerdict

The flow-verdict component (codebase_rag/flow_verdict.py) answers the question "can data flow from source to sink?" by querying the FLOWS_TO subgraph and performing reachability analysis.

The Cypher Query Pattern

The system retrieves candidate edges using a Cypher query that scopes results to the target project:

CYPHER_FLOW_EDGES = f"""
MATCH (a)-[:{cs.RelationshipType.FLOWS_TO.value}]->(b)
WHERE a.qualified_name STARTS WITH $project_prefix
   OR b.qualified_name STARTS WITH $project_prefix
   OR a.qualified_name = $project_name
   OR b.qualified_name = $project_name
RETURN a.qualified_name AS source, b.qualified_name AS target
"""

Verdict Categories

The flow_reachability_verdict function implements a BFS over the retrieved edges and returns one of three verdicts:

  • FOUND – A concrete path exists from the source qualified name to the sink qualified name through FLOWS_TO edges.
  • UNKNOWN – No path is found, but coverage gaps exist (files not indexed for flow analysis). This prevents false negatives when the analysis is incomplete.
  • NO_FLOW – No path exists and no coverage gaps are present, confirming the taint cannot reach the sink.

The function returns a FlowVerdict tuple containing the verdict, the concrete path (if any), and any uncovered files.

Security Vulnerability Detection

Security findings are expressed as HAS_VULNERABILITY relationships pointing to :SecurityIssue nodes. The graph audit queries (codebase_rag/cypher_queries.py) merge FLOWS_TO paths with vulnerability tags to validate exploits.

For example, the system can determine whether a secret from process.env reaches a network-send sink by checking for a FLOWS_TO path from the ENV source to the NETWORK sink that is also tagged with a vulnerability. This correlation ensures only reachable taint flows surface as security issues, avoiding false positives from unreachable or dead code.

from codebase_rag.flow_verdict import flow_reachability_verdict

def check_env_to_network(fetch_all, project_name):
    source = "ENV"          # qualified name of the source resource node

    sink   = "NETWORK"      # qualified name of the sink resource node

    verdict = flow_reachability_verdict(
        fetch_all=fetch_all,
        project_name=project_name,
        source_qn=source,
        sink_qn=sink,
    )
    print(f"Verdict: {verdict.verdict}")
    if verdict.verdict == "FOUND":
        print("Path:", " → ".join(verdict.path))
    elif verdict.verdict == "UNKNOWN":
        print("Uncovered files:", ", ".join(verdict.gaps))

Summary

  • The FlowProcessor in codebase_rag/parsers/flow_access/processor.py generates FLOWS_TO edges during intra-procedural walks, using path-sensitive analysis for Python and lean analysis for other languages.
  • Taint originates from pseudo-origins seeded from function parameters and free variables, then propagates through resource-to-resource, caller-to-callee, and callee-to-caller edges.
  • The FLOWS_TO relationship type is defined in codebase_rag/constants/graph.py and stored only when the IO capture group is enabled.
  • The flow_reachability_verdict function in codebase_rag/flow_verdict.py queries these edges and returns FOUND, UNKNOWN, or NO_FLOW verdicts based on BFS reachability and coverage gap analysis.
  • Security vulnerabilities are confirmed by correlating FLOWS_TO paths with HAS_VULNERABILITY relationships, ensuring only reachable taint flows generate alerts.

Frequently Asked Questions

What is the difference between lean flow and Python-specific flow analysis?

In codebase_rag/parsers/flow_access/processor.py, non-Python languages use a lean flow walk (_process_lean_flow) that performs basic taint tracking without branch sensitivity. For Python, the processor implements a path-sensitive MAY walk that evaluates conditional branches separately and unions taint states at merge points, providing more precise results for Python's complex control flow.

How does the system avoid false positives in vulnerability detection?

The flow_reachability_verdict function distinguishes between NO_FLOW (definitely safe) and UNKNOWN (insufficient coverage) verdicts. By requiring a concrete BFS path through FLOWS_TO edges from source to sink, and by checking for coverage gaps, the system only reports FOUND when the vulnerability is provably reachable in the analyzed code, filtering out false positives from dead or unindexed code.

What types of sources and sinks does FLOWS_TO track?

The processor loads language-specific IO_SINKS definitions that categorize calls as read-sources (where taint enters), write-sinks (where taint exiting is dangerous), or read-write sinks. Common examples include ENV for environment variables, STDOUT for output streams, and NETWORK for socket operations, though the exact definitions are configurable per language.

When are FLOWS_TO edges actually written to the graph?

Edges are deferred during the intra-procedural walk and materialized only when finalize() is called on the FlowProcessor instance. This occurs after the per-function return-taint fixpoint is computed, ensuring that all return edges and cross-function flows accurately reflect the final taint state of the function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →