VDB
Sign up
Worked example

One file, start to finish

Eighteen lines of ordinary Flask, a lockfile, and the whole loop: what VDB finds, what left the machine to find it, the fix, and the signed evidence that the path is closed. Every block below is copied from a real run — including the one that finds nothing.

The file

Nothing exotic

A handler that proxies an avatar, a loader for an uploaded profile, and a health check. No CVE is involved in anything below: both packages are pinned at versions that do exactly what their documentation says.

import requests
import yaml
from flask import request


def fetch_avatar():
    """Proxy an avatar for the profile page."""
    url = request.args["avatar_url"]
    return requests.get(url).content


def load_profile(blob):
    """Read a profile document a user uploaded."""
    return yaml.load(blob)


def health():
    return requests.get("https://status.internal.example/health").json()

The lockfile is what decides the answer — a version range cannot be analyzed, because the answer differs per version.

requests==2.19.0
urllib3==1.23
PyYAML==5.3.1
idna==2.7
chardet==3.0.4
certifi==2026.7.22
Step 1

Run it

vdb harden app.py --manifest requirements.txt
2 call site(s) with a decided dataflow path (4 path(s) in total, independently of whether any CVE exists)
6 dependencies on the graph · 6 analysed · taint reached 4
scope: dataflow into your dependencies, and your own standard-library calls that run
commands, deserialize, decompress or open connections (subprocess, eval/exec, pickle,
zlib/zipfile/tarfile, urlopen). File paths (open, os.path) and regular expressions in
your own code are not examined — use a static analyser such as Bandit or Semgrep for those.

* app.py:9  urllib3.util.parse_url  [urllib3@1.23]
   net_request  confidence 0.8 · 2 argument(s)
     args     : url
     fix      : scheme-allowlist, host-allowlist, block-internal-ranges
     residual : install-script malware, background data exfiltration, unanalyzed dynamic
                loading, dynamic dispatch on the path, DNS can change between validation
                and connection, redirects are not re-validated
     path_id  : P-4927330b4da0-net_request, P-76acd2ecd8ee-net_request

* app.py:14  yaml.load  [pyyaml@5.3.1]
   cmd_exec  confidence 0.8
     args     : stream
     fix      : allowlist-check
     residual : install-script malware, background data exfiltration, unanalyzed dynamic
                loading, dynamic dispatch on the path
     path_id  : P-4db57f067a93-cmd_exec
   global_state  confidence 0.8
     args     : stream
     fix      : key-allowlist
     residual : install-script malware, background data exfiltration, unanalyzed dynamic
                loading, dynamic dispatch on the path
     path_id  : P-6c534d09df6d-global_state

The report is grouped by call site — the line you will edit — however many arguments or sink kinds meet there. Two things worth reading twice. The network path runs through requests into urllib3 — the package the risk actually lands in is one nobody wrote down. And the fix line names three defenses, not one: a scheme check alone is not an SSRF fix.

What it did not flag: health()calls the same requests.getwith a literal URL and produces no path. That is the only line on this page that proves the tool is deciding rather than pattern-matching imports.

Step 2

See what left the machine

--emit-ir prints exactly what is sent about your code and makes no network call, so you can read it before you send anything. The only other things sent are the lockfile and a project name for alerts (the folder name unless you pass--project). The file name is F0001, the variables are v1 andv2, and the source isEXT. No source text, no identifiers, no paths, and no literal values — a file-open mode such as"rb" is the one literal sent as written, and there is none in this file.

{
  "version": 1,
  "language": "python",
  "files": [
    "F0001"
  ],
  "unparsed": [],
  "sources": [
    {
      "symbol": "v1",
      "kind": "input"
    },
    {
      "symbol": "v2",
      "kind": "param"
    }
  ],
  "flows": [
    {
      "from": "EXT",
      "to": "v1",
      "transform": "identity",
      "kind": "assign"
    }
  ],
  "callsites": [
    {
      "id": "c1",
      "module": "requests",
      "api": "get",
      "symbol": "f1#requests.get",
      "line": 9,
      "args": [
        {
          "index": 0,
          "name": "0",
          "symbol": "v1",
          "tainted": true,
          "transform": "identity",
          "sanitizers": []
        }
      ]
    },
    /* c2: yaml.load (line 14), c3: the literal-URL requests.get (line 18) */
  ],
  "masked_literals": 2,
  "removed_constants": 0,
  "loss": 0.25
}

The abstraction still reveals dependency and API names, argument positions and graph edges. Treat those as project metadata when deciding whether to use the hosted service.

Step 3

Take the fix

--patch prints a wrapper for your call site. Note what it is not: a patched, forked or pinned dependency. You usually cannot fix someone else's package; you can always fix your own boundary.

def vdb_safe_url(value, allowed_hosts=()):
    """Scheme/host allowlist plus internal-range blocking."""
    import ipaddress, socket
    from urllib.parse import urlparse
    u = urlparse(str(value))
    if u.scheme not in ("http", "https"):
        raise ValueError("scheme not allowed")
    if not allowed_hosts:
        raise ValueError("a non-empty host allowlist is required")
    if u.hostname not in allowed_hosts:
        raise ValueError("host not in allowlist")
    try:
        addresses = {row[4][0] for row in socket.getaddrinfo(
            u.hostname or "", u.port or (443 if u.scheme == "https" else 80),
            type=socket.SOCK_STREAM)}
    except (socket.gaierror, UnicodeError):
        raise ValueError("host does not resolve")
    def _unwrap(ip):
        # An IPv6 address can carry an IPv4 one inside it. Checking only the
        # outer form let 6to4 through: 2002:7f00:0001:: is 127.0.0.1 wearing
        # an address family that answers is_global with True.
        if ip.version == 6:
            if ip.ipv4_mapped:
                return ip.ipv4_mapped
            if ip.sixtofour:
                return ip.sixtofour
            if ip.teredo:
                return ip.teredo[1]
        return ip
    for addr in addresses:
        ip = _unwrap(ipaddress.ip_address(addr))
        # is_global is the allowlist, not is_private the blocklist. The old
        # flag list missed carrier-grade NAT (100.64.0.0/10) entirely, which
        # is routable inside an ISP or a cloud tenancy and is exactly where a
        # neighbour's metadata service lives.
        if not ip.is_global or ip.is_multicast:
            raise ValueError("non-public address blocked")
    return value

# At the call site: wrap the argument (the dependency itself is never modified)
# get(vdb_safe_url(v1, allowed_hosts=("api.example.com",)), ...)

Applied, the handler becomes one line longer:

def fetch_avatar():
    url = request.args["avatar_url"]
    return requests.get(vdb_safe_url(url, allowed_hosts=ALLOWED_HOSTS)).content
Step 4

Prove you closed it

vdb harden app.py --manifest requirements.txt \
  --verify P-4927330b4da0-net_request
path P-4927330b4da0-net_request -> CLOSED (sanitizer-applied:block-internal-ranges,host-allowlist,scheme-allowlist)
  graph hash : 8e25c9dd6228dd7c...
  decided at : 2026-08-28T03:14:48Z
  signature  : 541aee2e69f232e02d7cc4a7...
  note       : This authenticates VDB's decision over the submitted abstract IR.
               It does not prove that the IR faithfully represents a deployed
               build. When closure rests on a wrapper, only an exact
               reviewed-wrapper AST is credited.

The evidence is bound to the graph hash it was decided against, so an attestation cannot be quietly reused after a dependency moves. Anyone can check the signature at POST /v1/harden/evidence/verifywithout the signing key.

Edit the wrapper and it stops counting. Delete the internal-range loop, keep the scheme and host checks, and you do not get two thirds of a pass:

path P-4927330b4da0-net_request -> OPEN (no-sanitizer-at-boundary)

Credit is all-or-nothing on an exact match with the reviewed implementation. A scheme check with the internal ranges left open is the exact shape of a working SSRF, and partial credit is how an attestation that calls it closed gets minted. Function names and the word allowlist are not accepted as proof either.

What this example does not show

  • Closure is not proof of safety. A boundary fix constrains what your argument can do. It does not make the dependency trustworthy, and every answer carries a residual-risk block naming what it cannot cover.
  • A missed relation is a false negative. Reachability comes from static per-package summaries. Treat the output as evidence for triage order, not proof of non-exploitability.
  • Python and PyPI today. That is the declared scope of this feature.
  • The signature authenticates a decision, not a deployment. It proves VDB decided this over the submitted abstraction; it does not prove the abstraction matches what you shipped.
How the decision is made →API reference →Get an API key →