Security Auditor
Overview
The Security Auditor skill provides a structured approach to identifying security vulnerabilities in application code, based on the OWASP Top 10 and secure coding best practices. It covers input validation, injection attacks (SQL, command, LDAP), cross-site scripting (XSS), cross-site request forgery (CSRF), authentication and session flaws, insecure direct object references, secrets in code, and dependency vulnerabilities. Every finding includes a severity rating, a clear explanation of the risk, and a concrete remediation.
When to Use
- Reviewing code before deployment for security flaws
- Auditing authentication, authorization, or session management logic
- Checking a login form, API endpoint, or data processing pipeline
- Reviewing code that handles user-supplied input
- Checking for hardcoded secrets or insecure dependency versions
When NOT to Use
- Network-level security assessments (firewall rules, TLS config)
- Penetration testing or active exploitation (separate discipline)
- Infrastructure security (cloud IAM policies, container security)
- Compliance audits (SOC2, HIPAA, PCI-DSS) — these require human auditors
Quick Reference
| # | Category | Key Check |
|---|---|---|
| A01 | Broken Access Control | Is every route/endpoint authorization-checked? |
| A02 | Cryptographic Failures | Is sensitive data encrypted in transit and at rest? |
| A03 | Injection | Is ALL user input parameterized/sanitized? |
| A04 | Insecure Design | Are threat models documented and addressed? |
| A05 | Security Misconfiguration | Debug mode off in prod? Default passwords changed? |
| A06 | Vulnerable Components | Are dependencies up to date and scanned? |
| A07 | Auth/Identity Failures | Strong passwords, MFA, secure sessions? |
| A08 | Software/Data Integrity | Are dependencies verified with checksums/locks? |
| A09 | Logging/Monitoring Failures | Are security events logged without leaking sensitive data? |
| A10 | SSRF | Are outbound requests to user-supplied URLs restricted? |
Instructions
Map the attack surface
- List all inputs: query params, request bodies, headers, cookies, file uploads, env vars.
- List all outputs: HTML responses, JSON API responses, file writes, database writes.
- List all trust boundaries: anonymous vs. authenticated, user vs. admin, client vs. server.
Check for injection vulnerabilities (A03)
- SQL Injection: Are all database queries using parameterized queries or prepared statements? No string concatenation with user input.
- Command Injection: Is
os.system(),exec(),subprocessever called with user-controlled data? - LDAP/XPath/NoSQL Injection: Same principle — parameterize all queries.
- Template Injection: Are user strings ever passed to template engines without escaping?
Check for XSS (output encoding)
- Is user-supplied text escaped before rendering in HTML? (
<,>,&) - Is
innerHTMLordangerouslySetInnerHTMLused with user data? - Are Content Security Policy (CSP) headers set?
- Are
HttpOnlyandSecureflags on session cookies?
- Is user-supplied text escaped before rendering in HTML? (
Check for authentication and session flaws (A07)
- Are passwords hashed with bcrypt, scrypt, or Argon2 (not MD5/SHA1)?
- Are session tokens sufficiently random and at least 128 bits?
- Is there a session invalidation mechanism on logout?
- Are failed login attempts rate-limited or locked after N failures?
- Is multi-factor authentication available for sensitive operations?
Check for broken access control (A01)
- Is every endpoint that returns data verifying the requester owns/can access that data?
- Are admin-only endpoints checking for admin role, not just login?
- Are direct object references (IDs in URLs) validated against the authenticated user?
- Is
CORSconfigured to only allow trusted origins?
Check for secrets in code
- Search for:
api_key,secret,password,token,credentialin source files. - Check
.envfiles committed to version control (git log --all -S "password"). - Verify
.gitignoreexcludes all secret files. - Use environment variables or a secrets manager (Vault, AWS Secrets Manager, GitHub Secrets).
- Search for:
Check CSRF protection
- Are state-changing requests (POST, PUT, DELETE) protected with CSRF tokens or SameSite cookies?
- Is the CSRF token validated server-side on every mutation?
Check for SSRF (A10)
- Is user-supplied URL ever fetched server-side without validation?
- Are private IP ranges (10.x, 172.16.x, 192.168.x, 169.254.x) blocked?
Check for insecure dependencies (A06)
- Run
npm audit,pip-audit,bundler-audit, ortrivyon dependencies. - Check for packages with known CVEs. Update or replace vulnerable versions.
- Run
Assign severity and remediate
- Critical: Exploitable without authentication; direct data breach or RCE risk.
- High: Exploitable with authentication; significant data exposure.
- Medium: Requires specific conditions; partial data exposure.
- Low: Defense-in-depth improvement; no direct exploit path.
Examples
Example 1: Audit a login function
Input:
import sqlite3
import hashlib
def login(username, password):
conn = sqlite3.connect('app.db')
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor = conn.execute(query)
user = cursor.fetchone()
if user:
session['user_id'] = user[0]
session['role'] = user[4]
return True
return False
Output:
[Critical] SQL Injection — Line 5
The username and password are interpolated directly into the SQL string. An attacker can log in as any user with username = admin'-- (comments out the password check).
Attack: username = "admin'--"
Query becomes: SELECT * FROM users WHERE username = 'admin'--' AND password = '...'
Fix:
cursor = conn.execute(
"SELECT * FROM users WHERE username = ? AND password_hash = ?",
(username, hash_password(password))
)
[Critical] Plaintext password comparison — Line 5
Passwords appear to be stored and compared in plaintext. A database breach exposes all passwords immediately.
Fix: Hash passwords with bcrypt at registration; compare with bcrypt.checkpw() at login:
import bcrypt
def hash_password(password: str) -> bytes:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt())
def verify_password(password: str, hashed: bytes) -> bool:
return bcrypt.checkpw(password.encode(), hashed)
[High] No rate limiting / brute force protection
An attacker can make unlimited login attempts. No lockout, delay, or CAPTCHA is implemented.
Fix: Implement exponential backoff or lock after 5 failed attempts per username per 15 minutes using Redis or a failed_logins table.
[Medium] Role stored in session without verification
session['role'] is set from the database but should be re-verified on each sensitive request, not trusted from the session store alone.
[Low] Database connection not closed
conn is not closed, potentially exhausting connections under load. Use a context manager:
with sqlite3.connect('app.db') as conn:
...
Example 2: Review an API endpoint for security
Input:
// Express.js endpoint
app.get('/api/documents/:id', async (req, res) => {
const docId = req.params.id;
const doc = await db.query(`SELECT * FROM documents WHERE id = ${docId}`);
if (!doc) return res.status(404).json({ error: 'Not found' });
// Return document including internal metadata
res.json(doc);
});
Output:
[Critical] SQL Injection — Line 3
docId is interpolated directly into the SQL query. An attacker can pass 1 OR 1=1 to return all documents.
Fix:
const doc = await db.query('SELECT * FROM documents WHERE id = $1', [docId]);
[Critical] Missing authorization check There is no check that the requesting user owns or has permission to view this document. Any authenticated (or unauthenticated) user can access any document by guessing its ID (Insecure Direct Object Reference — IDOR). Fix:
app.get('/api/documents/:id', authenticate, async (req, res) => {
const doc = await db.query(
'SELECT * FROM documents WHERE id = $1 AND owner_id = $2',
[req.params.id, req.user.id]
);
if (!doc.rows[0]) return res.status(404).json({ error: 'Not found' });
res.json(doc.rows[0]);
});
[High] Authentication middleware missing
The authenticate middleware is not applied. Any anonymous request succeeds.
[Medium] Sensitive fields in response
SELECT * returns all columns including internal fields (e.g., storage_path, internal_tags). Return only fields the client should see:
const { id, title, content, created_at } = doc.rows[0];
res.json({ id, title, content, created_at });
[Low] Missing input validation on id parameter
req.params.id should be validated as a positive integer or UUID before use, providing defense-in-depth even with a parameterized query.
Best Practices
- Validate ALL inputs — length, type, format, range — before using them
- Use parameterized queries everywhere, no exceptions
- Apply the principle of least privilege: services, database users, and API keys should have minimum required permissions
- Store secrets in environment variables or a secrets manager — never in source code or version control
- Set security headers:
Content-Security-Policy,X-Frame-Options,X-Content-Type-Options,Strict-Transport-Security - Log security events (failed logins, authorization failures, input validation failures) without including sensitive data
Common Mistakes
- Trusting client-supplied data (headers, cookies, params) for authorization decisions
- Using MD5 or SHA1 for password hashing (they are not password hashing algorithms)
- Exposing stack traces or internal error details in API responses
- Disabling CORS entirely instead of configuring it correctly
- Storing session tokens in
localStorage(vulnerable to XSS) instead ofHttpOnlycookies - Not rotating secrets after a potential exposure
Tips & Tricks
- Use
git log --all -S "password"to find secrets ever committed to the repo semgrep(free) can automatically find common vulnerability patterns in source code- OWASP ZAP is a free tool for automated scanning of web application endpoints
npm audit fix/pip-audit --fixcan automatically upgrade vulnerable dependencies- OWASP's Cheat Sheet Series (cheatsheetseries.owasp.org) has ready-to-use remediation patterns