zebbern/secure-code-review
Systematically reviews code for SQL injection, XSS, SSRF, broken access control, cryptographic failures, and other common OWASP Top 10 vulnerabilities, providing vulnerable code examples and ready-to-use remediation guidance. Trigger this skill when users ask for a security review, vulnerability scan, or penetration testing assistance, or mention keywords like OWASP, SQL injection, XSS, code audit, or security checklist.
npx skills add https://github.com/zebbern/claude-code-guide --skill secure-code-review
A systematic security review based on the OWASP Top 10 (2021) standard. Each item includes: vulnerability description, typical vulnerable code, inspection checkpoints, and remediation examples. Designed for security-focused code review of web applications.
Provide the code files or code snippets to review, and specify which OWASP categories to check (or request a full review) to receive an item-by-item audit report.
Example prompts:
| ID | Category | Key Check |
|----|----------|-----------|
| A01 | Broken Access Control | Does every endpoint verify the current user's identity? Can users access others' data by changing IDs? |
| A02 | Cryptographic Failures | Are passwords hashed with bcrypt/argon2? Are secrets hardcoded? |
| A03 | Injection | String-concatenated SQL? shell=True? Unescaped template output? |
| A04 | Insecure Design | Is rate limiting in place? Can critical workflows be bypassed? |
| A05 | Security Misconfiguration | DEBUG enabled? Stack traces in error pages? Default credentials? |
| A06 | Vulnerable Components | Any CVEs from pip audit / npm audit? |
| A07 | Authentication Failures | Is JWT signature verified? Can tokens be revoked? Is MFA available? |
| A08 | Integrity Failures | Any pickle.loads deserializing untrusted data? |
| A09 | Logging & Monitoring Failures | Are plaintext passwords in logs? Are failed logins recorded? |
| A10 | SSRF | Are user-supplied URLs filtered against internal IPs? |
Core principle: prefer false positives over missed true positives.
file:line_numberRisk: Users can access other users' data or perform unauthorized operations.
Checkpoints:
# ❌ Vulnerable: No authorization check — any user can view others' orders by changing user_id
@app.route("/api/orders/<user_id>")
def get_orders(user_id):
orders = db.query(f"SELECT * FROM orders WHERE user_id = {user_id}")
return jsonify(orders)
# ✅ Fixed: Verify the authenticated user can only access their own data
@app.route("/api/orders")
@login_required
def get_orders():
current_user_id = get_current_user().id
orders = db.query("SELECT * FROM orders WHERE user_id = %s", (current_user_id,))
return jsonify(orders)
Risk: Sensitive data (passwords, credit card numbers, personal information) is unencrypted or uses weak cryptographic algorithms.
Checkpoints:
# ❌ Vulnerable: MD5 for password storage, hardcoded secret key
import hashlib
SECRET_KEY = "my-secret-key-123"
def save_password(password):
hashed = hashlib.md5(password.encode()).hexdigest()
db.save(hashed)
# ✅ Fixed: bcrypt for password hashing, secret key from environment variable
import bcrypt
import os
SECRET_KEY = os.environ["SECRET_KEY"]
def save_password(password):
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode(), salt)
db.save(hashed)
Risk: User input is concatenated directly into SQL, OS commands, LDAP queries, etc., allowing attackers to execute arbitrary queries or commands.
Checkpoints:
os.system() or subprocess.call(shell=True) calls that concatenate user input?# ❌ Vulnerable: String-concatenated SQL — attacker can input ' OR 1=1 --
@app.route("/api/user")
def get_user():
username = request.args.get("username")
query = f"SELECT * FROM users WHERE username = '{username}'"
result = db.execute(query)
return jsonify(result)
# ✅ Fixed: Parameterized query
@app.route("/api/user")
def get_user():
username = request.args.get("username")
result = db.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
return jsonify(result)
# ❌ Vulnerable: User input concatenated directly into shell command
import os
def ping_host(host):
os.system(f"ping -c 4 {host}")
# ✅ Fixed: Use subprocess with list arguments, shell disabled
import subprocess
import re
def ping_host(host):
if not re.match(r'^[a-zA-Z0-9.\-]+$', host):
raise ValueError("Invalid hostname")
subprocess.run(["ping", "-c", "4", host], check=True)
Risk: Business logic design flaws that cannot be fixed by a perfect implementation.
Checkpoints:
# ❌ Vulnerable: No attempt limit on verification code — can be brute-forced
@app.route("/api/verify-code", methods=["POST"])
def verify_code():
code = request.json["code"]
stored_code = session.get("verification_code")
if code == stored_code:
return jsonify({"status": "verified"})
return jsonify({"status": "invalid"}), 400
# ✅ Fixed: Added attempt limit and expiration
@app.route("/api/verify-code", methods=["POST"])
def verify_code():
attempts = session.get("verify_attempts", 0)
if attempts >= 5:
return jsonify({"error": "Too many attempts, please request a new code"}), 429
code = request.json["code"]
stored = session.get("verification_code")
expire_at = session.get("code_expire_at", 0)
if time.time() > expire_at:
return jsonify({"error": "Verification code has expired"}), 400
session["verify_attempts"] = attempts + 1
if code == stored:
session.pop("verify_attempts", None)
return jsonify({"status": "verified"})
return jsonify({"status": "invalid"}), 400
Risk: Applications or servers use default configurations, enable unnecessary features, or expose sensitive information in error messages.
Checkpoints:
# ❌ Vulnerable: DEBUG enabled in production, leaking sensitive information
app = Flask(__name__)
app.config["DEBUG"] = True
app.config["SECRET_KEY"] = "default-secret"
@app.errorhandler(500)
def error_handler(e):
return jsonify({"error": str(e), "traceback": traceback.format_exc()}), 500
# ✅ Fixed: Configuration from environment variables, DEBUG off in production
import os
app = Flask(__name__)
app.config["DEBUG"] = os.environ.get("FLASK_DEBUG", "false").lower() == "true"
app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]
@app.errorhandler(500)
def error_handler(e):
app.logger.error(f"Internal error: {e}")
return jsonify({"error": "Internal server error, please try again later"}), 500
Risk: Using third-party libraries or framework versions with known vulnerabilities.
Checkpoints:
pip audit, npm audit, snyk, etc.)?# Python projects
pip audit
# Node.js projects
npm audit
# General scanning
# Use open-source tools like trivy or grype to scan container/project dependencies
# Update vulnerable packages
pip install --upgrade package_name
# Auto-fix npm vulnerabilities
npm audit fix
# Pin dependency versions to prevent implicit upgrades
pip freeze > requirements.txt
Risk: Authentication mechanisms have flaws that allow brute-force attacks, credential stuffing, or session hijacking.
Checkpoints:
# ❌ Vulnerable: JWT signature not verified, accepts alg=none
import jwt
def verify_token(token):
payload = jwt.decode(token, options={"verify_signature": False})
return payload
# ✅ Fixed: Enforce signature and expiration verification, specify algorithm
import jwt
import os
JWT_SECRET = os.environ["JWT_SECRET"]
def verify_token(token):
try:
payload = jwt.decode(
token,
JWT_SECRET,
algorithms=["HS256"],
options={"require": ["exp", "iat", "sub"]}
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError("Token has expired")
except jwt.InvalidTokenError:
raise AuthError("Invalid token")
Risk: Failure to verify the integrity of software updates, critical data, or CI/CD pipelines, enabling supply chain attacks or data tampering.
Checkpoints:
pickle.loads on untrusted data)?# ❌ Vulnerable: Deserializing untrusted data — can lead to remote code execution
import pickle
@app.route("/api/import", methods=["POST"])
def import_data():
data = pickle.loads(request.data)
process(data)
return "OK"
# ✅ Fixed: Use a safe data format (JSON), refuse to deserialize arbitrary objects
import json
@app.route("/api/import", methods=["POST"])
def import_data():
try:
data = json.loads(request.data)
except json.JSONDecodeError:
return jsonify({"error": "Invalid JSON format"}), 400
process(data)
return "OK"
Risk: Lack of security event logging and monitoring, preventing timely detection and response to attacks.
Checkpoints:
# ❌ Vulnerable: No logging on login failure, and plaintext password in logs
def login(username, password):
user = db.get_user(username)
if not user or not check_password(password, user.password_hash):
print(f"Login failed for {username} with password {password}")
return None
return create_session(user)
# ✅ Fixed: Log security events without logging sensitive data
import logging
security_logger = logging.getLogger("security")
def login(username, password):
user = db.get_user(username)
if not user or not check_password(password, user.password_hash):
security_logger.warning(
"Login failed",
extra={"username": username, "ip": request.remote_addr}
)
return None
security_logger.info(
"Login successful",
extra={"username": username, "ip": request.remote_addr}
)
return create_session(user)
Risk: The application accepts user-provided URLs and makes server-side requests, allowing attackers to access internal network resources or cloud metadata.
Checkpoints:
# ❌ Vulnerable: Directly requesting user-supplied URL — can access internal network and cloud metadata
import requests
@app.route("/api/fetch-url")
def fetch_url():
url = request.args.get("url")
response = requests.get(url)
return response.text
# ✅ Fixed: Validate URL protocol and target address, block internal network access
import requests
import ipaddress
from urllib.parse import urlparse
import socket
BLOCKED_NETWORKS = [
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 is_safe_url(url):
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return False
try:
ip = ipaddress.ip_address(socket.gethostbyname(parsed.hostname))
for network in BLOCKED_NETWORKS:
if ip in network:
return False
except (socket.gaierror, ValueError):
return False
return True
@app.route("/api/fetch-url")
def fetch_url():
url = request.args.get("url")
if not is_safe_url(url):
return jsonify({"error": "Access to this address is not allowed"}), 403
response = requests.get(url, timeout=10, allow_redirects=False)
return response.text
After completing the review, output a report in the following format. All 10 items must appear — mark items with no findings as pass:
# OWASP Top 10 Security Review Report
## Review Summary
- Scope: [list of files/modules]
- Date: [date]
- Risk summary: RED High x | YELLOW Medium x | GREEN Low x | PASS No findings x
## Findings (sorted by severity, descending)
### [Severity] [OWASP ID] — [Issue Title]
- **Location:** [file:line_number]
- **Description:** [issue description]
- **Impact:** [potential consequences]
- **Fix:** (ready-to-use code, not just a description)
### PASS A0X — [Category] — No issues found
## Remediation Priority
1. [Most urgent fix — rationale]
2. [Next priority — rationale]
3. ...
| Tool | Language | Purpose |
|------|----------|---------|
| bandit | Python | Python code security scanning |
| semgrep | Multi-language | Rule-based code scanning |
| eslint-plugin-security | JavaScript | JS security rules |
| npm audit / pip audit | JS / Python | Dependency vulnerability scanning |
| trivy | Multi-language | Container and dependency scanning |
| sqlmap | — | SQL injection detection |
| OWASP ZAP | — | Web application dynamic scanning |
> Note: This checklist is a supplementary review tool and does not replace professional penetration testing. For high-security systems, combine automated scanning + manual code audit + penetration testing.
Take zebbern/secure-code-review from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.