Secure Coding Validator
When to activate
Use this skill when:
- Reviewing code for common OWASP Top 10 vulnerabilities (injection, XSS, broken auth, sensitive data exposure)
- Validating third-party or untrusted code contributions before merge
- Checking a PR for compliance with secure coding standards (input validation, parameterized queries, secret hygiene)
- Auditing specific security-sensitive subsystems (auth handlers, payment processing, database layers)
- Verifying CORS configurations, API authentication, or secret management patterns
When NOT to use
- For full penetration testing or runtime security validation—this skill reviews code patterns, not behavior
- When you need architectural threat modeling—escalate to
/security-reviewor a threat modeling specialist - For compliance audits requiring signed attestation or formal reporting
- To debug active security incidents—focus on containment first, then code review
- When analyzing obfuscated, compiled, or binary code (no source available)
Instructions
1. Input Validation Patterns
Scan for:
Unvalidated user input — Check for direct use of
request.args,request.form,query params, orbodywithout sanitization- Look for missing length checks, type validation, whitelist constraints
- Flag regex patterns that are too permissive or use user input in regex (
eval,sprintfinto regex)
Type coercion issues — Verify types are explicitly checked, not coerced silently
- Python:
int(user_input)without try/except - JavaScript: loose equality (
==) instead of strict (===) - SQL: numeric fields that accept string concatenation
- Python:
Boundary conditions — Check for off-by-one errors, negative inputs, null handling
- Array index validation before access
- File path traversal (
../../../etc/passwdpatterns)
Fix pattern:
# Bad
@app.route('/user/<user_id>')
def get_user(user_id):
return db.query(f"SELECT * FROM users WHERE id = {user_id}")
# Good
@app.route('/user/<user_id>')
def get_user(user_id):
try:
user_id = int(user_id)
if user_id <= 0:
return error(400)
return db.query("SELECT * FROM users WHERE id = ?", (user_id,))
except ValueError:
return error(400)
2. Output Encoding
Scan for:
HTML/XML context escaping — Check templates and string concatenation for unescaped user data
- Python: Jinja2
autoescape=True, Flaskmarkupsafe.escape() - JavaScript: textContent vs innerHTML; DOMPurify for user-generated HTML
- Flag direct
+concatenation of user input into HTML
- Python: Jinja2
URL encoding — Verify query params and path components are percent-encoded
- Flag bare string interpolation in redirects
JSON context — Ensure JSON is properly escaped if rendered in HTML context
- Flag
<script>tags containing unescaped JSON
- Flag
CSV/Excel injection — Flag rows starting with
=,+,@,-without escaping
Fix pattern:
// Bad
document.getElementById('user-name').innerHTML = userInput;
// Good
document.getElementById('user-name').textContent = userInput;
// OR if HTML is intentional, sanitize:
import DOMPurify from 'dompurify';
document.getElementById('user-name').innerHTML = DOMPurify.sanitize(userInput);
3. SQL Injection Prevention
Scan for:
String concatenation in queries — Flag f-strings, format(), or
+operators in SQL- Exception: DDL that genuinely requires dynamic table/column names (requires allowlist + escape)
Missing parameterization — Verify all user input uses
?,:param, or ORM equivalents- SQL:
cursor.execute("... WHERE id = ?", (user_id,)) - ORM: SQLAlchemy
session.query().filter_by(), DjangoModel.objects.filter()
- SQL:
Stored procedure/callable injection — Check if stored proc parameters are user-controlled
Fix pattern:
# Bad
query = f"SELECT * FROM orders WHERE user_id = {user_id}"
cursor.execute(query)
# Good
cursor.execute("SELECT * FROM orders WHERE user_id = ?", (user_id,))
4. CORS and Cross-Origin Policy
Scan for:
Overly permissive CORS — Flag
Access-Control-Allow-Origin: *with credentials- Should use explicit origin whitelist
Missing CORS headers — For APIs, check
Access-Control-Allow-Methods,Access-Control-Allow-HeadersWildcard in allowlist — Flag
*.example.comif it includes subdomains you don't control
Fix pattern:
# Bad
@app.after_request
def set_cors(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Credentials'] = 'true'
return response
# Good
ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com']
@app.after_request
def set_cors(response):
origin = request.headers.get('Origin')
if origin in ALLOWED_ORIGINS:
response.headers['Access-Control-Allow-Origin'] = origin
response.headers['Access-Control-Allow-Credentials'] = 'true'
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
return response
5. Secret Management
Scan for:
Hardcoded secrets — API keys, passwords, tokens in code, config files, or comments
- Flag strings matching common secret patterns:
api_key=,password=,token=,secret=
- Flag strings matching common secret patterns:
Secrets in environment — Verify they use environment variables or secret manager, not
.envin repo- Check
.gitignoreincludes.env,.env.local, etc.
- Check
Secrets in logs — Look for print statements, logging, or error messages that may leak credentials
- Flag
logger.info(f"User logged in: {password}")patterns
- Flag
Overly permissive secret access — Check IAM policies (AWS, GCP) for broad secret reader roles
Fix pattern:
# Bad
DATABASE_URL = "postgresql://user:password123@localhost:5432/db"
API_KEY = "sk-12345abcde"
# Good
import os
DATABASE_URL = os.getenv('DATABASE_URL')
API_KEY = os.getenv('API_KEY')
# In .env (gitignored):
# DATABASE_URL=postgresql://user:password123@localhost:5432/db
# API_KEY=sk-12345abcde
6. Validation Checklist
For each code section:
- All user input is validated (type, length, format, range)
- All output is encoded for its context (HTML, URL, JSON, CSV)
- All database queries use parameterized statements
- CORS policies are explicitly configured, not wildcard
- No secrets are hardcoded; all use environment variables or secret manager
- Secrets are not logged, displayed in errors, or leaked in comments
- Exception handling doesn't expose internal details
- Authentication/authorization is checked before processing
- Sensitive data is encrypted at rest and in transit (TLS/HTTPS)
- Rate limiting and account lockout are in place for login/sensitive endpoints
Example
Before (Vulnerable Code)
from flask import Flask, request
import sqlite3
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
password = request.form.get('password')
# SQL injection
conn = sqlite3.connect('users.db')
query = f"SELECT id FROM users WHERE username = '{username}' AND password = '{password}'"
result = conn.execute(query).fetchone()
if result:
response = f"Welcome {username}!" # XSS if username contains HTML
return response
return "Invalid credentials"
@app.route('/api/user/<user_id>')
def get_user_api(user_id):
# No CORS configuration
return {"user_id": user_id}
# Hardcoded secrets
DB_PASSWORD = "super_secret_123"
API_KEY = "sk-live-12345abcde"
After (Secure)
from flask import Flask, request, jsonify
from flask_cors import CORS
from markupsafe import escape
import sqlite3
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
# Explicit CORS configuration
CORS(app, origins=['https://app.example.com'], supports_credentials=True)
# Load secrets from environment
DB_PASSWORD = os.getenv('DB_PASSWORD')
API_KEY = os.getenv('API_KEY')
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
# Input validation
if not username or not password:
return jsonify({"error": "Missing credentials"}), 400
if len(username) > 100 or len(password) > 255:
return jsonify({"error": "Invalid input"}), 400
# Parameterized query (SQL injection prevention)
try:
conn = sqlite3.connect('users.db')
query = "SELECT id FROM users WHERE username = ? AND password = ?"
result = conn.execute(query, (username, password)).fetchone()
conn.close()
except sqlite3.Error as e:
# Don't expose internal error; log separately
app.logger.error(f"DB error (not shown to user): {e}")
return jsonify({"error": "Authentication failed"}), 401
if result:
# Output encoding (escape HTML)
safe_username = escape(username)
response = jsonify({"message": f"Welcome {safe_username}!"})
return response
return jsonify({"error": "Invalid credentials"}), 401
@app.route('/api/user/<user_id>')
def get_user_api(user_id):
# Input validation
try:
user_id = int(user_id)
if user_id <= 0:
return jsonify({"error": "Invalid user ID"}), 400
except ValueError:
return jsonify({"error": "Invalid user ID"}), 400
# Parameterized query
try:
conn = sqlite3.connect('users.db')
query = "SELECT id, name FROM users WHERE id = ?"
result = conn.execute(query, (user_id,)).fetchone()
conn.close()
except sqlite3.Error:
return jsonify({"error": "Error retrieving user"}), 500
if not result:
return jsonify({"error": "User not found"}), 404
return jsonify({"id": result[0], "name": result[1]})
if __name__ == '__main__':
app.run(ssl_context='adhoc') # Enforce HTTPS
Key improvements:
- SQL queries use parameterized statements (
?placeholders) - User input is validated for type, length, and format
- Output is escaped for HTML context (
escape()) - CORS is explicitly configured, not wildcard
- Secrets are loaded from environment (
os.getenv()) - Errors don't expose internal database details
- HTTPS is enforced (ssl_context)