Security Review Checklist for PRs
Checklist for security checks during pull request review: input validation, authentication, authorization, secrets, dependencies, injection, XSS, CSRF, logging, and automated tooling integration with code examples for secure patterns.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
This checklist provides security review guidelines for pull requests. Every PR that modifies application code, configuration, or infrastructure must pass a security review before merging. This document covers input validation, authentication, authorization, secrets management, dependency changes, injection prevention, XSS, CSRF, logging, and automated tooling integration.
1. Input Validation
1.1 Checklist
- [ ] All user input is validated on the server side
- [ ] Input validation uses allowlist (not blocklist) approach
- [ ] Input length is bounded (max length enforced)
- [ ] Input type is verified (string, int, UUID, etc.)
- [ ] Input format is validated (regex for emails, URLs, dates)
- [ ] File uploads check MIME type and file extension
- [ ] File uploads limit maximum file size
- [ ] SQL queries use parameterized statements (no string concatenation)
- [ ] NoSQL queries prevent operator injection (e.g., $where, $expr)
- [ ] Command execution uses shell=False (no shell=True)
- [ ] XML parsing disables external entity expansion (XXE)
- [ ] JSON parsing limits depth and key count
1.2 Code Example — Input Validation
from pydantic import BaseModel, validator, constr, conint
from typing import Optional
import uuid
class CreateUserRequest(BaseModel):
username: constr(min_length=3, max_length=32, pattern=r'^[a-zA-Z0-9_]+$')
email: constr(min_length=5, max_length=254, pattern=r'^[^@]+@[^@]+\.[^@]+$')
age: conint(ge=13, le=120)
bio: Optional[constr(max_length=500)] = None
@validator('username')
def no_sql_keywords(cls, v):
# Additional check for SQL keywords (defense in depth)
blocked = {'select', 'insert', 'update', 'delete', 'drop', 'union'}
if any(kw in v.lower() for kw in blocked):
raise ValueError('Username contains reserved word')
return v
# Usage — invalid input raises ValidationError
try:
user = CreateUserRequest(**request.json())
except ValidationError as e:
return jsonify({'error': 'Invalid input', 'details': e.errors()}), 400
2. Authentication and Authorization
2.1 Checklist
- [ ] New endpoints require authentication (no unauthenticated access unless explicit)
- [ ] Authorization checks are enforced server-side (not just client-side)
- [ ] Resource ownership is verified (IDOR check)
- [ ] Permission checks use deny-by-default
- [ ] Role changes require admin authorization
- [ ] Session tokens are rotated after privilege change
- [ ] Password changes require current password verification
- [ ] API endpoints enforce rate limiting
- [ ] Sensitive operations require MFA re-verification
- [ ] JWT tokens are validated (signature, expiry, issuer, audience)
2.2 Code Example — Authorization Check
from functools import wraps
def authorize(action: str, resource_type: str = None):
def decorator(f):
@wraps(f)
def wrapped(*args, **kwargs):
user = g.current_user
resource_id = kwargs.get('id')
# Check permission
if not user.can(action, resource_type, resource_id):
log_denial(user, action, resource_type, resource_id)
abort(403)
return f(*args, **kwargs)
return wrapped
return decorator
@app.route('/api/orders/<uuid:order_id>', methods=['PUT'])
@authenticate
@authorize('update', 'order')
def update_order(order_id):
order = Order.query.get_or_404(order_id)
# IDOR check — verify ownership
if order.user_id != g.current_user.id and not g.current_user.has_role('admin'):
abort(403)
order.update(request.json)
return jsonify(order.to_dict())
3. Secrets Management
3.1 Checklist
- [ ] No secrets, API keys, or passwords in source code
- [ ] No secrets in configuration files committed to git
- [ ] Secrets are loaded from environment variables or secrets manager
- [ ] .env files are in .gitignore
- [ ] No secrets in log statements or error messages
- [ ] No secrets in URL parameters or query strings
- [ ] CI/CD pipeline uses secret variables (not hardcoded)
- [ ] Docker images don't contain secrets (use build args or runtime env)
- [ ] Secret scanning tool (GitLeaks, TruffleHog) is in pre-commit hook
- [ ] No secrets in comments or documentation
3.2 Pre-Commit Secret Scanning
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
args: ['--config', '.gitleaks.toml']
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
4. Dependency Changes
4.1 Checklist
- [ ] New dependencies are from trusted sources (official registries)
- [ ] New dependencies have no known critical vulnerabilities
- [ ] Dependency versions are pinned (no wildcards or ranges)
- [ ] License compatibility is verified (no GPL in commercial projects)
- [ ] Dependency is actively maintained (last commit < 1 year)
- [ ] No unnecessary dependencies added (justify each new dep)
- [ ] Dependency scan (Dependabot, Snyk) passes
- [ ] Transitive dependencies reviewed for known issues
- [ ] Lock file is updated (package-lock.json, poetry.lock, etc.)
- [ ] No dev dependencies moved to production dependencies
4.2 Automated Dependency Check in CI
# GitHub Actions — dependency security check
name: Dependency Security
on: [pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: npm audit
run: npm audit --audit-level=high --omit=dev
continue-on-error: false
- name: Snyk scan
uses: snyk/actions/node@master
with:
command: test
args: --severity-threshold=high --fail-on=issues
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: License check
run: npx license-checker --production --failOn 'GPL-3.0;AGPL-3.0'
5. Injection Prevention
5.1 Checklist
- [ ] SQL queries use parameterized statements or ORM
- [ ] No string concatenation for SQL queries
- [ ] No use of eval() or Function() with user input
- [ ] No use of innerHTML with user input (use textContent)
- [ ] No use of document.write() with user input
- [ ] Template engines use auto-escaping (Jinja2, Twig, etc.)
- [ ] Shell commands use argument arrays (not string concatenation)
- [ ] LDAP queries escape special characters
- [ ] XPath queries use parameterized expressions
- [ ] NoSQL queries sanitize $ operators
5.2 Code Examples
# BAD: SQL injection
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# GOOD: parameterized query
cursor.execute("SELECT * FROM users WHERE name = %s", (name,))
# BAD: command injection
import subprocess
subprocess.run(f"ls {user_dir}", shell=True)
# GOOD: argument array
subprocess.run(["ls", user_dir], shell=False)
# BAD: XSS in JavaScript
document.getElementById('output').innerHTML = userInput;
# GOOD: textContent
document.getElementById('output').textContent = userInput;
6. XSS and CSRF Prevention
6.1 XSS Checklist
- [ ] Output encoding applied to all user-generated content
- [ ] Content-Security-Policy header is set (no unsafe-inline)
- [ ] X-XSS-Protection header is set
- [ ] X-Content-Type-Options: nosniff is set
- [ ] Cookies are HttpOnly and Secure
- [ ] Framework auto-escaping is enabled
- [ ] No use of dangerouslySetInnerHTML (React) or [innerHTML] (Angular)
- [ ] User input in URLs is encoded (encodeURIComponent)
- [ ] SVG uploads are sanitized (remove script tags)
- [ ] Markdown rendering sanitizes HTML (no raw HTML)
6.2 CSRF Checklist
- [ ] CSRF token is included in all state-changing forms
- [ ] CSRF token is validated on the server side
- [ ] Cookies use SameSite=Strict or SameSite=Lax
- [ ] API endpoints use custom headers (not just cookies)
- [ ] Origin/Referer header is validated on state-changing requests
- [ ] CORS policy is restrictive (no wildcard origins)
- [ ] Login endpoint is CSRF-protected (login CSRF)
- [ ] Logout endpoint is CSRF-protected
6.3 Code Example — CSRF Protection
from flask import session
import secrets
def generate_csrf_token():
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return session['csrf_token']
@app.before_request
def validate_csrf():
if request.method in ('POST', 'PUT', 'PATCH', 'DELETE'):
token = request.headers.get('X-CSRF-Token') or request.form.get('csrf_token')
if not token or token != session.get('csrf_token'):
abort(403, description='CSRF token validation failed')
7. Logging and Error Handling
7.1 Checklist
- [ ] No sensitive data in log messages (passwords, tokens, PII)
- [ ] Error messages don't expose internal details to users
- [ ] Stack traces are logged server-side, not shown to clients
- [ ] Security events are logged (login, access denied, input validation)
- [ ] Logs include user ID, timestamp, source IP, and action
- [ ] Log injection is prevented (sanitize log input)
- [ ] Error handling doesn't swallow exceptions silently
- [ ] Debug mode is disabled in production
- [ ] Custom error pages don't leak server information
- [ ] Audit trail for all state-changing operations
7.2 Code Example — Safe Logging
import logging
import re
logger = logging.getLogger(__name__)
# Sanitize log input to prevent log injection
def sanitize_for_log(value: str) -> str:
# Remove newlines and carriage returns
return re.sub(r'[\r\n]', '', str(value))[:200]
def log_security_event(user_id: str, action: str, resource: str, ip: str):
logger.info(
"Security event: user=%s action=%s resource=%s ip=%s",
sanitize_for_log(user_id),
sanitize_for_log(action),
sanitize_for_log(resource),
sanitize_for_log(ip),
)
# BAD: logging sensitive data
logger.info(f"User logged in with password: {password}") # NEVER DO THIS
# GOOD: log without sensitive data
logger.info(f"User {user_id} logged in from {ip}")
8. Automated Tooling
8.1 Required CI Checks
Tool | Purpose | When to run | Block merge on
──────────────────┼────────────────────────────┼────────────────────┼──────────────────
SAST (Semgrep) | Static security analysis | Every PR | High severity
Snyk / Dependabot | Dependency vulnerabilities | Every PR | High severity
GitLeaks | Secret scanning | Every commit | Any finding
ESLint security | JS/TS security rules | Every PR | Error level
Bandit (Python) | Python security linter | Every PR | High severity
Trivy | Container image scan | Image build | High severity
OWASP ZAP | DAST (dynamic scan) | Nightly on staging | Critical
8.2 Semgrep Configuration
# semgrep.yml — custom security rules
rules:
- id: no-hardcoded-secrets
pattern: password = "..."
message: "Hardcoded password detected"
severity: ERROR
languages: [python, javascript, java]
- id: no-sql-injection
pattern: cursor.execute(f"...")
message: "Possible SQL injection — use parameterized queries"
severity: ERROR
languages: [python]
- id: no-shell-true
pattern: subprocess.run(..., shell=True)
message: "shell=True is dangerous with user input"
severity: WARNING
languages: [python]
- id: no-innerhtml
pattern: $EL.innerHTML = $INPUT
message: "innerHTML with user input causes XSS — use textContent"
severity: ERROR
languages: [javascript]
FAQ
When should I request a security review?
Request a security review for any PR that: adds authentication or authorization logic, handles user input, modifies security configuration, adds new dependencies, changes data access patterns, implements crypto or token handling, modifies CI/CD pipelines, or touches infrastructure configuration. For routine changes (UI styling, documentation, test additions), a self-review against this checklist is sufficient. When in doubt, request a review — it is cheaper than a security incident.
How long should a security review take?
A security review for a typical PR (100-500 lines changed) should take 30-60 minutes. For larger PRs or security-sensitive changes (auth, crypto, payment), allow 2-4 hours. The reviewer should run automated tools first (SAST, dependency scan), then manually review the checklist items. If the PR introduces significant security risk, schedule a dedicated review session with the security team rather than doing it inline.
What should I do if I find a security issue during review?
If the issue is critical (remote code execution, authentication bypass, data exposure), block the PR and notify the security team immediately. Do not merge until the issue is fixed. If the issue is high severity (SQL injection, XSS, access control), request changes and explain the fix needed. If the issue is medium or low, add a comment with the recommendation and let the author decide whether to fix in this PR or a follow-up. Never approve a PR with known critical or high security issues.
How do I balance security review with development speed?
Automate as much as possible — SAST, dependency scanning, and secret scanning should run automatically in CI and block PRs with critical findings. This catches the most common issues without manual review. For manual review, focus on the high-risk areas: authentication, authorization, input validation, and secrets. Use this checklist as a quick reference — don’t review every line for every item. Prioritize based on the PR’s risk profile. A PR that adds a new API endpoint needs more security review than one that changes button colors.
What tools should be in my security CI pipeline?
At minimum: SAST (Semgrep or SonarQube), dependency scanning (Snyk or Dependabot), and secret scanning (GitLeaks). Add DAST (OWASP ZAP) for staging environments. Add container scanning (Trivy) if you build Docker images. Add license checking (license-checker) for compliance. Configure all tools to fail the CI build on high-severity findings. Run SAST and secret scanning on every PR. Run dependency scanning on every PR and nightly. Run DAST nightly against staging. Review tool findings weekly to tune false positives.
See Also
Related Resources
OWASP Top 10 Remediation Checklist
Checklist for tracking OWASP Top 10 vulnerability remediation per application: risk assessment, fix priority, code-level remediation steps, verification testing, and compliance reporting with examples for each OWASP category.
DocSecrets Rotation Runbook
Runbook for rotating secrets without downtime: secret inventory, rotation schedule, zero-downtime rotation strategies, dual-key periods, automated rotation with AWS Secrets Manager and HashiCorp Vault, and emergency rotation procedures.
DocAPI Authentication Design Template
Template for documenting API authentication flows and token lifecycle: auth scheme selection, token types, issuance, validation, refresh, revocation, MFA, OAuth2 flows, JWT configuration, and security best practices with code examples.