feat: vendorizar as 10 skills do pilar de seguranca em skills/

Copia local (nao fork, nao submodule) de safedeps, sca-audit,
secrets-scan, code-review-security, api-security-review,
web-security-review, cve-triage, patch-prioritization,
dependency-scanning e hermaguard -- commit exato de origem e licenca
de cada uma em skills/NOTICE.md. Elimina o passo de clone externo
pro pilar de seguranca do wf-security-review: git clone deste repo
ja traz tudo funcional.

Corrige tambem o comando de instalacao pro Claude Code nos dois
READMEs e no docs/SKILLS.md: Claude Code exige um diretorio por
skill (skills/<nome>/SKILL.md), nao aceita os .md soltos que o
comando antigo copiava -- confirmado testando a instalacao real.
This commit is contained in:
evandrodevbr committed 2026-08-24 00:32:20 -03:00
1 parent ee44fa0f7a
commit c17192c9fe
130 files changed
+10533 -33

No files matched your search

@@ -0,0 +1,8 @@
import sqlite3
def get_user_by_name(username: str):
conn = sqlite3.connect("users.db")
cur = conn.cursor()
query = f"SELECT * FROM users WHERE name = '{username}'"
cur.execute(query)
return cur.fetchone()
@@ -0,0 +1,9 @@
{
"title": "SQL injection in get_user_by_name",
"description": "Username interpolated into SQL via f-string; attacker-controlled input reaches the query unsanitised.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{"file": "app.py", "trigger": "f-string SQL query username interpolation", "description": "SQL injection via unparameterised query", "severity": "CRITICAL", "classes": ["CWE-89"]}
]
}
@@ -0,0 +1,10 @@
--- a/app.py
+++ b/app.py
@@ -2,6 +2,6 @@ import sqlite3
def get_user_by_name(username: str):
conn = sqlite3.connect("users.db")
cur = conn.cursor()
- cur.execute("SELECT * FROM users WHERE name = ?", (username,))
+ query = f"SELECT * FROM users WHERE name = '{username}'"
+ cur.execute(query)
return cur.fetchone()
@@ -0,0 +1,5 @@
import yaml
def load_config(path: str) -> dict:
with open(path) as f:
return yaml.load(f)
@@ -0,0 +1,9 @@
{
"title": "Unsafe YAML deserialisation",
"description": "yaml.load() without SafeLoader allows arbitrary object construction — RCE via crafted YAML.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{"file": "app.py", "trigger": "yaml.load without SafeLoader", "description": "Arbitrary code execution via unsafe deserialisation", "severity": "CRITICAL", "classes": ["CWE-502"]}
]
}
@@ -0,0 +1,7 @@
--- a/app.py
+++ b/app.py
@@ -2,4 +2,4 @@ import yaml
def load_config(path: str) -> dict:
with open(path) as f:
- return yaml.safe_load(f)
+ return yaml.load(f)
@@ -0,0 +1,4 @@
def get_user_email(cur, user_id: int) -> str:
cur.execute("SELECT email FROM users WHERE id = ?", (user_id,))
row = cur.fetchone()
return row[0]
@@ -0,0 +1,9 @@
{
"title": "Missing null check on fetchone result",
"description": "fetchone() returns None when no row matches; indexing row[0] then raises TypeError, crashing the caller.",
"severity": "MEDIUM",
"category": "logic",
"findings": [
{"file": "app.py", "trigger": "cursor fetchone row indexed without null check", "description": "TypeError crash when user_id not found", "severity": "MEDIUM", "classes": ["CWE-476", "CWE-754"]}
]
}
@@ -0,0 +1,8 @@
--- a/app.py
+++ b/app.py
@@ -2,4 +2,3 @@ def get_user_email(cur, user_id: int) -> str:
cur.execute("SELECT email FROM users WHERE id = ?", (user_id,))
row = cur.fetchone()
- if row is None:
- return None
return row[0]
@@ -0,0 +1,4 @@
import subprocess
def ping_host(host: str) -> bytes:
return subprocess.check_output(f"ping -c 1 {host}", shell=True)
@@ -0,0 +1,9 @@
{
"title": "Shell command injection via ping utility",
"description": "User-supplied host is concatenated into a shell command; shell metacharacters execute arbitrary commands.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{"file": "app.py", "trigger": "user host concatenated into shell command", "description": "OS command injection via shell=True with unescaped input", "severity": "CRITICAL", "classes": ["CWE-78"]}
]
}
@@ -0,0 +1,6 @@
--- a/app.py
+++ b/app.py
@@ -2,2 +2,2 @@ import subprocess
def ping_host(host: str) -> bytes:
- return subprocess.check_output(["ping", "-c", "1", host])
+ return subprocess.check_output(f"ping -c 1 {host}", shell=True)
@@ -0,0 +1,3 @@
def read_doc(name: str) -> str:
with open("/var/docs/" + name) as f:
return f.read()
@@ -0,0 +1,9 @@
{
"title": "Path traversal in file download",
"description": "User-supplied filename is joined without validation; ../ sequences escape the intended directory.",
"severity": "HIGH",
"category": "security",
"findings": [
{"file": "app.py", "trigger": "filename joined into path without traversal check", "description": "Arbitrary file read via ../ traversal", "severity": "HIGH", "classes": ["CWE-22"]}
]
}
@@ -0,0 +1,8 @@
--- a/app.py
+++ b/app.py
@@ -1,3 +1,3 @@
def read_doc(name: str) -> str:
- safe = os.path.basename(name)
- with open(os.path.join("/var/docs", safe)) as f:
+ with open("/var/docs/" + name) as f:
return f.read()
@@ -0,0 +1,11 @@
# Deliberately broken sample: a live-looking API key committed to source.
# NOTE: the value is an obviously-fake placeholder (marker, not a real key
# format) so secret scanners do not flag the corpus; the CWE-798 pattern
# (credential hardcoded in source) is what the benchmark tests for.
PAYMENT_KEY = "EXAMPLE-KEY-DO-NOT-USE-0123456789"
def charge(card_token: str, amount: int):
import requests
return requests.post("https://api.example-payments.invalid/v1/charges",
headers={"Authorization": f"Bearer {PAYMENT_KEY}"},
data={"card": card_token, "amount": amount})
@@ -0,0 +1,17 @@
{
"title": "Hard-coded API key committed to source",
"description": "Live API key embedded in source; anyone with repo access can use the account.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "live API key assigned to module constant",
"description": "Credential theft and unauthorized charges from public repo",
"severity": "CRITICAL",
"classes": [
"CWE-798"
]
}
]
}
@@ -0,0 +1,4 @@
--- a/app.py
+++ b/app.py
@@ -0,0 +1 @@
+ (new file — the bug is introduced by this change)
@@ -0,0 +1,5 @@
def delete_user(session_user, target_id: int):
# authn done upstream, but no role check
db.execute("DELETE FROM users WHERE id = ?", (target_id,))
db.commit()
return {"ok": True}
@@ -0,0 +1,18 @@
{
"title": "Admin endpoint without authorization check",
"description": "delete_user performs no permission check; any authenticated user can delete anyone.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "admin action without role/permission check",
"description": "Privilege escalation \u2014 any user deletes any account",
"severity": "CRITICAL",
"classes": [
"CWE-862",
"CWE-269"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.py
+++ b/app.py
def delete_user(session_user, target_id: int):
- if session_user.get("role") != "admin":
- raise PermissionError("admin only")
# authn done upstream, but no role check
@@ -0,0 +1,3 @@
def order_total(price_cents: int, quantity: int) -> int:
# no upper bound on quantity
return price_cents * quantity
@@ -0,0 +1,18 @@
{
"title": "Unvalidated quantity causes integer overflow",
"description": "quantity from request body multiplies price without bounds; extreme values overflow.",
"severity": "MEDIUM",
"category": "logic",
"findings": [
{
"file": "app.py",
"trigger": "quantity multiplied without upper-bound check",
"description": "Overflow/wraparound corrupts totals on extreme input",
"severity": "MEDIUM",
"classes": [
"CWE-190",
"CWE-20"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.py
+++ b/app.py
def order_total(price_cents: int, quantity: int) -> int:
- if quantity > 10_000:
- raise ValueError("quantity too large")
# no upper bound on quantity
@@ -0,0 +1,5 @@
import requests
def send_webhook(url: str, payload: dict):
# user-supplied URL fetched without validation
requests.post(url, json=payload, timeout=5)
@@ -0,0 +1,18 @@
{
"title": "SSRF via user-controlled webhook URL",
"description": "Webhook URL comes from the request and is fetched server-side without allowlist.",
"severity": "HIGH",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "user-supplied URL fetched server-side without allowlist",
"description": "SSRF \u2014 internal network probing and metadata endpoint access",
"severity": "HIGH",
"classes": [
"CWE-918",
"CWE-20"
]
}
]
}
@@ -0,0 +1,8 @@
--- a/app.py
+++ b/app.py
import requests
def send_webhook(url: str, payload: dict):
- if not url.startswith("https://hooks.example.com/"):
- raise ValueError("webhook host not allowed")
# user-supplied URL fetched without validation
@@ -0,0 +1,5 @@
function renderResults(results, searchTerm) {
const box = document.getElementById("results");
box.innerHTML = `<h2>Results for ${searchTerm}</h2>` +
results.map(r => `<li>${r.name}</li>`).join("");
}
@@ -0,0 +1,17 @@
{
"title": "Reflected XSS via innerHTML assignment",
"description": "Search term inserted into the DOM via innerHTML without escaping.",
"severity": "HIGH",
"category": "security",
"findings": [
{
"file": "app.js",
"trigger": "user input interpolated into innerHTML",
"description": "Script injection in victim browsers via crafted search term",
"severity": "HIGH",
"classes": [
"CWE-79"
]
}
]
}
@@ -0,0 +1,4 @@
--- a/app.js
+++ b/app.js
@@ -0,0 +1 @@
+ (new file — the bug is introduced by this change)
@@ -0,0 +1,6 @@
def withdraw(conn, user_id: int, amount: int):
row = conn.execute("SELECT balance FROM accounts WHERE id = ?", (user_id,)).fetchone()
if row[0] < amount:
raise ValueError("insufficient funds")
conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, user_id))
conn.commit()
@@ -0,0 +1,17 @@
{
"title": "TOCTOU race on balance check",
"description": "Check-then-deduct without lock or atomic update; concurrent requests double-spend.",
"severity": "HIGH",
"category": "concurrency",
"findings": [
{
"file": "app.py",
"trigger": "balance check and deduct are separate non-atomic statements",
"description": "Double-spend under concurrent requests",
"severity": "HIGH",
"classes": [
"CWE-362"
]
}
]
}
@@ -0,0 +1,11 @@
--- a/app.py
+++ b/app.py
def withdraw(conn, user_id: int, amount: int):
row = conn.execute("SELECT balance FROM accounts WHERE id = ?", (user_id,)).fetchone()
- # single atomic guarded update
- cur = conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?", (amount, user_id, amount))
- if cur.rowcount == 0:
- raise ValueError("insufficient funds")
- conn.commit()
- return
if row[0] < amount:
@@ -0,0 +1,5 @@
async function loadProfile(userId) {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
return data;
}
@@ -0,0 +1,18 @@
{
"title": "Unhandled promise rejection",
"description": "fetch promise has no catch; network failure leaves UI in loading state forever.",
"severity": "MEDIUM",
"category": "async",
"findings": [
{
"file": "app.js",
"trigger": "await fetch chain with no try/catch or .catch",
"description": "Unhandled rejection \u2014 loading state never resolves on failure",
"severity": "MEDIUM",
"classes": [
"HG-ASYNC",
"CWE-754"
]
}
]
}
@@ -0,0 +1,11 @@
--- a/app.js
+++ b/app.js
async function loadProfile(userId) {
- try {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
return data;
- } catch (e) {
- showError(e);
- }
}
@@ -0,0 +1,6 @@
def process_payment(order):
try:
gateway.charge(order)
except Exception:
pass
return {"status": "paid"}
@@ -0,0 +1,18 @@
{
"title": "Swallowed exception in payment processing",
"description": "Bare except: pass hides every failure; function returns fake success.",
"severity": "HIGH",
"category": "error-handling",
"findings": [
{
"file": "app.py",
"trigger": "bare except pass, returns success regardless",
"description": "Fake success masks failed charges \u2014 silent data/money loss",
"severity": "HIGH",
"classes": [
"HG-DEGENERATE",
"CWE-754"
]
}
]
}
@@ -0,0 +1,10 @@
--- a/app.py
+++ b/app.py
def process_payment(order):
try:
gateway.charge(order)
except Exception:
- log.exception("charge failed")
- return {"status": "failed"}
+ pass
return {"status": "paid"}
@@ -0,0 +1,4 @@
def transfer(conn, from_id, to_id, amount):
conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, from_id))
conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, to_id))
conn.commit()
@@ -0,0 +1,18 @@
{
"title": "Non-atomic multi-step update",
"description": "Two-table update without transaction; crash between statements corrupts state.",
"severity": "HIGH",
"category": "data-integrity",
"findings": [
{
"file": "app.py",
"trigger": "two dependent updates without a wrapping transaction",
"description": "Crash mid-sequence loses money \u2014 debit without credit",
"severity": "HIGH",
"classes": [
"HG-PARTIAL-WRITE",
"CWE-362"
]
}
]
}
@@ -0,0 +1,7 @@
--- a/app.py
+++ b/app.py
def transfer(conn, from_id, to_id, amount):
- with conn:
conn.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (amount, from_id))
conn.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (amount, to_id))
- conn.commit()
@@ -0,0 +1,4 @@
def read_config(path):
f = open(path)
data = json.load(f)
return data
@@ -0,0 +1,18 @@
{
"title": "Unclosed file handle on error path",
"description": "open() without context manager; exception path leaks the descriptor.",
"severity": "MEDIUM",
"category": "resources",
"findings": [
{
"file": "app.py",
"trigger": "open without with/close on all paths",
"description": "FD leak under repeated failures exhausts process limit",
"severity": "MEDIUM",
"classes": [
"CWE-404",
"CWE-754"
]
}
]
}
@@ -0,0 +1,7 @@
--- a/app.py
+++ b/app.py
def read_config(path):
- with open(path) as f:
- return json.load(f)
+ f = open(path)
data = json.load(f)
@@ -0,0 +1,5 @@
def handler(event):
try:
return do_work(event)
except Exception as e:
return {"statusCode": 500, "body": str(e)}
@@ -0,0 +1,17 @@
{
"title": "Stack trace returned to client",
"description": "Bare handler returns exception text in the HTTP response body.",
"severity": "MEDIUM",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "exception text returned in response body",
"description": "Internal paths, queries, and versions leaked to clients",
"severity": "MEDIUM",
"classes": [
"CWE-200"
]
}
]
}
@@ -0,0 +1,4 @@
--- a/app.py
+++ b/app.py
@@ -0,0 +1 @@
+ (new file — the bug is introduced by this change)
@@ -0,0 +1,8 @@
def parse(stream):
records = []
while True:
line = stream.readline()
if not line:
break
records.append(json.loads(line))
return records
@@ -0,0 +1,17 @@
{
"title": "Unbounded loop over user input",
"description": "parse runs until end-of-stream with no cap on iterations or size.",
"severity": "MEDIUM",
"category": "resources",
"findings": [
{
"file": "app.py",
"trigger": "read loop without record-count or byte cap",
"description": "Memory exhaustion DoS from large uploads",
"severity": "MEDIUM",
"classes": [
"CWE-400"
]
}
]
}
@@ -0,0 +1,12 @@
--- a/app.py
+++ b/app.py
def parse(stream):
records = []
+ MAX = 10_000
while True:
line = stream.readline()
if not line:
break
+ if len(records) >= MAX:
+ raise ValueError("too many records")
records.append(json.loads(line))
@@ -0,0 +1,6 @@
import hashlib
def verify(shared_secret: str, provided: str) -> bool:
a = hashlib.sha256(shared_secret.encode()).hexdigest()
b = hashlib.sha256(provided.encode()).hexdigest()
return a == b
@@ -0,0 +1,18 @@
{
"title": "Password comparison not constant-time",
"description": "Plain == on password-derived token allows timing side channel.",
"severity": "MEDIUM",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "secret compared with plain string equality",
"description": "Timing side channel leaks secret one byte at a time",
"severity": "MEDIUM",
"classes": [
"CWE-287",
"CWE-200"
]
}
]
}
@@ -0,0 +1,10 @@
--- a/app.py
+++ b/app.py
import hashlib
+import hmac
def verify(shared_secret: str, provided: str) -> bool:
a = hashlib.sha256(shared_secret.encode()).hexdigest()
b = hashlib.sha256(provided.encode()).hexdigest()
- return hmac.compare_digest(a, b)
+ return a == b
@@ -0,0 +1,4 @@
import pickle
def load_state(body: bytes):
return pickle.loads(body)
@@ -0,0 +1,18 @@
{
"title": "Pickle load of user-supplied bytes",
"description": "pickle.loads on request body executes arbitrary code during deserialisation.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "pickle.loads on untrusted request bytes",
"description": "RCE \u2014 pickle deserialisation executes attacker code",
"severity": "CRITICAL",
"classes": [
"CWE-502",
"CWE-94"
]
}
]
}
@@ -0,0 +1,4 @@
--- a/app.py
+++ b/app.py
@@ -0,0 +1 @@
+ (new file — the bug is introduced by this change)
@@ -0,0 +1,4 @@
def reset_password(request, user_id: int):
new_pw = request.json["password"]
db.execute("UPDATE users SET pw = ? WHERE id = ?", (hash(new_pw), user_id))
db.commit()
@@ -0,0 +1,18 @@
{
"title": "State-changing route without authentication",
"description": "POST handler has no session check; anonymous requests reset any account.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "state-changing handler without session/auth check",
"description": "Account takeover by anonymous callers",
"severity": "CRITICAL",
"classes": [
"CWE-306",
"CWE-287"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.py
+++ b/app.py
def reset_password(request, user_id: int):
- if not request.session.get("user"):
- raise PermissionError("login required")
new_pw = request.json["password"]
@@ -0,0 +1,3 @@
function canEdit(order) {
return order.status == 1; // "1" also passes
}
@@ -0,0 +1,17 @@
{
"title": "Loose equality on status check",
"description": "JS == with string/number mix accepts wrong status values.",
"severity": "LOW",
"category": "logic",
"findings": [
{
"file": "app.js",
"trigger": "loose == between string and number",
"description": "Wrong-type status grants edit rights unexpectedly",
"severity": "LOW",
"classes": [
"CWE-20"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.js
+++ b/app.js
function canEdit(order) {
- return order.status === 1;
+ return order.status == 1; // "1" also passes
}
@@ -0,0 +1,8 @@
async function refresh() {
state.loading = true;
try {
state.data = await api.get("/data");
} catch (e) {
state.error = e.message;
}
}
@@ -0,0 +1,18 @@
{
"title": "Missing loading\u2192error UI transition",
"description": "Error path updates message but never clears the loading flag.",
"severity": "MEDIUM",
"category": "logic",
"findings": [
{
"file": "app.js",
"trigger": "catch path never resets loading flag",
"description": "Spinner stuck forever after a failed refresh",
"severity": "MEDIUM",
"classes": [
"HG-STATE",
"HG-ASYNC"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.js
+++ b/app.js
} catch (e) {
state.error = e.message;
+ state.loading = false;
}
@@ -0,0 +1,5 @@
def change_email(request):
if request.method in ("GET", "POST"):
u = db.get_user(request.args["user"])
u.email = request.args["email"]
u.save()
@@ -0,0 +1,17 @@
{
"title": "State change on GET without CSRF token",
"description": "Password change honors GET with no token; img tags can trigger it.",
"severity": "HIGH",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "state change accepted on GET without CSRF token",
"description": "CSRF \u2014 attacker sites change victim emails via img/link",
"severity": "HIGH",
"classes": [
"CWE-352"
]
}
]
}
@@ -0,0 +1,6 @@
--- a/app.py
+++ b/app.py
def change_email(request):
- if request.method == "POST" and request.form.get("csrf_token") == session_csrf():
+ if request.method in ("GET", "POST"):
u = db.get_user(request.args["user"])
@@ -0,0 +1,2 @@
def apply_filter(value, expr: str):
return eval(f"{value} {expr}")
@@ -0,0 +1,17 @@
{
"title": "eval on template expression",
"description": "Template renderer evals user-supplied filter expressions.",
"severity": "CRITICAL",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "eval on user-controlled expression string",
"description": "Arbitrary code execution via crafted filter",
"severity": "CRITICAL",
"classes": [
"CWE-94"
]
}
]
}
@@ -0,0 +1,4 @@
--- a/app.py
+++ b/app.py
@@ -0,0 +1 @@
+ (new file — the bug is introduced by this change)
@@ -0,0 +1,6 @@
import os
def save_upload(file_storage):
dest = os.path.join("/srv/uploads", file_storage.filename)
file_storage.save(dest)
return dest
@@ -0,0 +1,18 @@
{
"title": "Unrestricted upload accepts any type",
"description": "Upload saves whatever the client sends under the original extension.",
"severity": "HIGH",
"category": "security",
"findings": [
{
"file": "app.py",
"trigger": "upload saved without extension/content-type validation",
"description": "Webshell upload via .php/.jsp under web root",
"severity": "HIGH",
"classes": [
"CWE-434",
"CWE-22"
]
}
]
}
@@ -0,0 +1,9 @@
--- a/app.py
+++ b/app.py
import os
def save_upload(file_storage):
- ext = os.path.splitext(file_storage.filename)[1].lower()
- if ext not in (".png", ".jpg", ".pdf"):
- raise ValueError("file type not allowed")
dest = os.path.join("/srv/uploads", file_storage.filename)