Python static analysis, security-first
Pysa (Python Static Analyzer) is a security-focused static analysis tool built on Facebook’s Python type checker, Pyre. It has been released as open source under the Pyre repository, alongside a library of taint definitions for common web frameworks. The goal: detect and prevent security and privacy issues before code ever lands in a repository.
Pysa models many security and privacy vulnerabilities as data-flow problems — data originating from a source (e.g., user-controlled input) flowing into a sink (e.g., an API that executes code or accesses the file system). For example, Pysa is used at Facebook to check that Python code respects technical privacy policies enforced by internal frameworks, and to catch common web vulnerabilities such as XSS and SQL injection.
Pysa follows the template set by Zoncolan, Facebook’s static analyzer for Hack code that has scanned over 100 million lines and helped prevent thousands of security issues. Pysa shares algorithms and code with Zoncolan but targets Python, and its largest deployment runs against the millions of lines of Python powering Instagram’s servers. When run on a proposed code change, Pysa returns results in about an hour — compared to weeks or months of manual review — allowing issues to be caught before they propagate into the codebase.
Because Pysa ships with taint definitions for Django and Tornado, it can start flagging issues in projects that use these frameworks on the first run. For other frameworks, adding a few lines of configuration to specify where data enters the server is typically enough. Pysa was used to detect and report the open-source vulnerability CVE-2019-19775, and the Zulip open-source project has integrated it into its workflow.
How data flow analysis works
Pysa performs interprocedural analysis, meaning it can trace data flows across function calls. This requires mapping function invocations to their implementations using all available code information, including optional static types when present. Pyre provides that type information. Pysa and Pyre share a codebase but are separate products.
The analysis works in iterative rounds to build "summaries" of functions: which functions return data from a source, and which functions have parameters that eventually reach a sink. If a source connects to a sink, Pysa reports an issue. This can be visualized as a tree with the issue at the root and contributing sources and sinks at the leaves:

Handling false positives and negatives
Given security’s importance, Pysa is tuned to avoid false negatives — real issues missed — even if that means accepting more false positives, which are benign flows that get flagged. Too many false positives, however, can cause alert fatigue and risky signals.
Users can reduce false positives in two ways:
- Sanitizers: A direct mechanism that tells the analyzer to completely stop tracking a data flow after it passes through a designated function or attribute. This is used when a transformation guarantees the data is harmless in a given context.
- Features: Collected metadata attached to data flows during analysis. Features don’t result in any issues being removed from the results, but they allow post-hoc filtering on specific source-sink pairs where certain data has been rendered benign for one sink kind but not others.
Pysa on a real code path
Consider the effect of a proposed refactor:
Original code, in which a SQL injection in load_pictures is not exploitable because that function only ever receives a valid user_id derived from load_user, may pass scrutiny if configured to do so:
# views/user.py
async def get_profile(request: HttpRequest) -> HttpResponse:
profile = load_profile(request.GET['user_id'])
...
# controller/user.py
async def load_profile(user_id: str):
user = load_user(user_id) # Loads a user safely; no SQL injection
pictures = load_pictures(user.id)
...
# model/media.py
async def load_pictures(user_id: str):
query = f"""
SELECT *
FROM pictures
WHERE user_id = {user_id}
"""
result = run_query(query)
...
# model/shared.py
async def run_query(query: str):
connection = create_sql_connection()
result = await connection.execute(query)
...
Now suppose an engineer optimizes the controller to fetch user and picture data concurrently:
# controller/user.py
async def load_profile(user_id: str):
user, pictures = await asyncio.gather(
load_user(user_id),
load_pictures(user_id) # no longer 'user.id'!
)
...
The added code is the kind of change most engineers would consider innocuous, but it creates a path that connects user-controlled user_id to the SQL injection vulnerability in load_pictures. In a large application with layers between entry points and database queries, this path may be opaque to the author of the change — but not to Pysa. Placed in a proposal on the Instagram codebase, this change would be flagged because Pysa detects that data is flowing from user-controlled input directly into a SQL query.
What Pysa cannot do
Pysa is built to discover only data-flow-related security issues. No static analyzer can be perfect, and not all security or privacy issues reduce to data flows. Pysa is not the right tool for verifying that an authorization check, such as user_is_admin, fires before a privileged operation like delete_user. The issue may involve permission logic but no data flowing through that check. Code can sometimes be rewritten to model such checks in data-flow terms or to make them safer by embedding permission checks into the privileged operation, but Pysa alone won’t catch them.
There are also three practical constraints:
Performance and precision trade-offs
Pysa must finish its analysis before a developer’s change merges. To keep analysis fast, Pysa may simplify tracking when too many attributes of an object are tainted, treating the entire object as a source. This is conservative and can lead to false positives.
Support limits for Python features
Pysa doesn’t fully support decorators in its call graph when invoking decorated functions, so issues that appear inside decorators may pass through undetected.
Python dynamism
Python’s flexibility makes some static analysis very hard. Without type information, it can be impossible to determine which implementation of a method is called. Pysa can still find issues in fully untyped code — it has done so in production projects — but it's most effective when effort is invested in annotations for critical types.
Dynamic imports and attribute changes also create blind spots. For example, take code that dynamically imports os before making a sensitive call; Pysa won't track that the local variable refers to os:
def secret_eval(request: HttpRequest):
os = importlib.import_module("os")
# Pysa won't know what 'os' is, and thus won't
# catch this remote code execution issue
os.system(request.GET["command"])
Equally difficult are cases like ambiguous method dispatch:
class Bird:
def fly(self): ...
class Airplane:
def fly(self): ...
def take_off(x):
x.fly() # Which function does this call?
While Pysa could be extended to detect such patterns, Python supports endless near-equivalent pathological cases. Users should therefore expect that perfection is impossible, and pair automated analysis with other security measures.
What Pysa Caught in Production
Since deployment, Pysa has become a primary detection layer for the Instagram server codebase. In the first half of 2020, it identified 44 percent of all issues that engineers found in that code. That figure includes both pre-existing vulnerabilities and those introduced through new code changes.
Breaking down the results from proposed changes, Pysa reported 330 unique issues across all vulnerability categories. Of those, 49 (15 percent) were classified as significant. Another 131 (40 percent) were genuine problems but carried mitigating circumstances that reduced their severity. The remaining 150 (45 percent) were false positives—a rate the team accepts given the tool's deliberate bias toward avoiding false negatives.
The team regularly cross-checks Pysa's output against findings from other channels, such as the bug bounty program, to catch and correct false negatives. Each vulnerability type is individually tunable, and with ongoing refinement, the more mature checks now report 100 percent valid issues.
Iterating With Pysa
The trade-offs baked into Pysa were chosen to let security engineers scale their review effort, but the tool was built for continuous improvement rather than static operation. That design came from close collaboration between security and software engineers on the team, which allowed rapid iteration that an off-the-shelf product could not have supported. One concrete outcome of that collaboration is improved trace browsing, making it easier to distinguish real vulnerabilities from false positives during review.
Pysa follows the same philosophy as Zoncolan, Meta's static analysis tool for Hack, but applies it to Python. It automates the detection of security issues both in private codebases and in open source projects. The tool is freely available as open source, with documentation and a tutorial for getting started.
Pysa's development and deployment involved contributions from a large team, including Maxime Arthaud, Apurv Bhargava, Jia Chen, Manuel Fahndrich, Lorenzo Fontana, Dominik Gabi, Nolan Alexander Jimenez, Zack Landau, Mark Mendoza, Eray Mitrani, Ibrahim Mohamed, Maggie Moss, Radu Nesiu, Nicholas O’Brien, Edward Qiu, Pradeep Kumar Srinivasan, and Shannon Zhu.



