Skip to content

security-data-exfil

Load when reviewing code that handles URL fetching, file paths, raw SQL, XML parsing, response serialization, debug modes, or error handling that may expose internal data.

From plugin
vexjoy-agent
413198 skills198 agents10 commands86 hooks
Install
$ npx -y skills add notque/vexjoy-agent --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

Load when reviewing code that handles URL fetching, file paths, raw SQL, XML parsing, response serialization, debug modes, or error handling that may expose internal data.

Agent definition

security-data-exfil.md

Data Exfiltration Patterns

Load when reviewing code that handles URL fetching, file paths, raw SQL, XML parsing, response serialization, debug modes, or error handling that may expose internal data.

Data exfiltration: data crosses a trust boundary it should not. Validate inputs at the boundary, expose only fields the caller needs.

---

Validate URLs at the IP Layer Before Fetching

When fetching user-provided URLs (webhooks, image proxies, import-from-URL), validate the resolved IP against a blocklist of internal ranges. String-based URL checks fail because DNS rebinding, IP encoding, and redirects bypass hostname validation.

Correct Pattern

**Python:**

import ipaddress, socket
from urllib.parse import urlparse

BLOCKED_RANGES = [
    ipaddress.ip_network('127.0.0.0/8'),
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('172.16.0.0/12'),
    ipaddress.ip_network('192.168.0.0/16'),
    ipaddress.ip_network('169.254.0.0/16'),
]

def safe_fetch(user_url: str) -> bytes:
    parsed = urlparse(user_url)
    if parsed.scheme not in ('http', 'https'):
        raise ValueError('scheme not allowed')
    addr = socket.getaddrinfo(parsed.hostname, parsed.port or 443)[0][4][0]
    ip = ipaddress.ip_address(addr)
    if any(ip in net for net in BLOCKED_RANGES):
        raise ValueError('internal IP not allowed')
    resp = requests.get(user_url, allow_redirects=False, timeout=10)
    return resp.content

**TypeScript:**

import { lookup } from 'dns/promises';
import ipaddr from 'ipaddr.js';

const BLOCKED_RANGES = ['private', 'linkLocal', 'loopback', 'uniqueLocal', 'unspecified'];

async function safeFetch(userUrl: string): Promise<Response> {
  const parsed = new URL(userUrl);
  if (!['http:', 'https:'].includes(parsed.protocol)) throw new Error('scheme not allowed');
  const { address } = await lookup(parsed.hostname);
  const ip = ipaddr.parse(address);
  if (BLOCKED_RANGES.includes(ip.range())) throw new Error('internal IP not allowed');
  return fetch(parsed.href, { redirect: 'manual' });
}

Why This Matters

SSRF reaches internal services, cloud metadata (169.254.169.254 returns IAM credentials).

**CVEs:** Capital One 2019, CVE-2021-26855 (Exchange ProxyLogon), CVE-2024-34351 (Next.js Server Actions SSRF), CVE-2020-28168 (axios redirect bypass), CVE-2024-21893 (Ivanti SAML SSRF).

Detection

rg -n 'requests\.(get|post|put)\(|urlopen\(|urllib\.request' --type py
rg -n 'fetch\(|axios\.(get|post)\(|got\(' --type ts --type js
rg -n 'callback_url|webhook_url|target_url|redirect_url' --type py --type ts
rg -n 'allow_redirects=True' --type py

---

Contain File Paths with Realpath Validation

Resolve the full path and verify it stays within the intended base directory.

Correct Pattern

**Python:**

from pathlib import Path
from flask import abort, send_file

def serve_export(name: str):
    base = Path("/var/app/exports").resolve()
    target = (base / name).resolve()
    if not target.is_relative_to(base):
        abort(403)
    return send_file(target)

**TypeScript:**

import path from 'path';

function serveFile(name: string, res: Response) {
  const base = path.resolve('/var/app/exports');
  const target = path.resolve(base, name);
  if (!target.startsWith(base + path.sep)) return res.sendStatus(403);
  res.sendFile(target);
}

**Go:**

func serveExport(w http.ResponseWriter, r *http.Request) {
    base := "/var/app/exports"
    name := filepath.Base(r.URL.Query().Get("name"))
    target := filepath.Join(base, name)
    clean, err := filepath.EvalSymlinks(target)
    if err != nil || !strings.HasPrefix(clean, base) {
        http.Error(w, "forbidden", http.StatusForbidden)
        return
    }
    http.ServeFile(w, r, clean)
}

**Archive extraction (Python):**

import tarfile

def safe_extract(archive_path: str, dest: str):
    with tarfile.open(archive_path) as tar:
        tar.extractall(path=dest, filter="data")  # Python 3.12+

Why This Matters

Path traversal reads arbitrary files. Archive extraction compounds risk (zip-slip).

**CVEs:** CVE-2007-4559 (Python tarfile), CVE-2022-48285 (jszip), CVE-2023-26111 (node-static).

Detection

rg -n 'os\.path\.join\(|Path\(' --type py | rg -v 'resolve\(\)|realpath'
rg -n 'extractall\(' --type py | rg -v 'filter='
rg -n 'sendFile\(|readFile\(|readFileSync\(' --type ts
rg -n 'open\(.*request\.|open\(.*user_' --type py

---

Use Parameterized Queries for All Database Access

Pass user values as parameters, never interpolated strings.

Correct Pattern

**Python (Django):**

invoices = Invoice.objects.filter(customer_id=request.GET["cid"])

# When raw SQL unavoidable:
Invoice.objects.extra(where=["customer_id = %s"], params=[request.GET["cid"]])

**TypeScript (Prisma):**

const users = await prisma.$queryRaw`SELECT * FROM users WHERE name = ${name}`;
// NEVER use $queryRawUnsafe with user input

**Go:**

row := db.QueryRowContext(ctx,
    "SELECT * FROM orders WHERE id = $1 AND user_id = $2",
    orderID, userID,
)

Why This Matters

SQL injection via string interpolation remains one of the most exploited vulnerability classes. ORM escape hatches (`.raw()`, `.extra()`, `$queryRawUnsafe`) bypass parameterization.

**CVEs:** CVE-2023-25813 (Sequelize `literal()`), CVE-2025-23061 (Mongoose `populate({match: userObj})`).

Detection

rg -n '\.raw\(f"|\.extra\(where=\[f"|cursor\.execute\(f"|RawSQL\(f"' --type py
rg -n 'text\(f"|session\.execute\(f"' --type py
rg -n '\$queryRawUnsafe|\$executeRawUnsafe' --type ts
rg -n 'literal\(|sequelize\.query\(' --type ts --type js
rg -n 'fmt\.Sprintf.*SELECT|fmt\.Sprintf.*INSERT|fmt\.Sprintf.*UPDATE' --type go

---

Disable External Entity Resolution in XML Parsers

Configure XML parsers to reject DTDs and external entity references before parsing untrusted XML.

Correct Pattern

**Python:**

from defusedxml im
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.

Get the whole plugin, auto-invoked