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.
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow 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.mdData 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
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
Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

