Skip to content

Secure Coding Practices

Knowing about vulnerabilities is only half the battle. The other half is building secure habits into your daily development workflow so that vulnerabilities never make it into production in the first place. This page covers the practical techniques, tools, and checklists you need to write secure code consistently — not just when you remember to think about security.


Input Validation

Every piece of data that crosses a trust boundary — user input, API responses, file uploads, database results — must be treated as potentially malicious until validated. Input validation is your first line of defense against injection attacks, buffer overflows, and business logic exploits.

Allowlisting vs Denylisting

ApproachDescriptionEffectiveness
Allowlisting (preferred)Define exactly what IS allowed and reject everything elseHigh — new attack patterns are rejected by default
Denylisting (fragile)Define what is NOT allowed and accept everything elseLow — attackers constantly find new patterns to bypass the list

Always prefer allowlisting. Denylists are inherently incomplete because you cannot enumerate every possible malicious input.

Denylist approach (fragile):
Block: <script>, javascript:, onerror=, onload=...
Problem: Attacker uses <SCRIPT>, <img src=x onerror=...>, or encoding tricks
Allowlist approach (robust):
Allow: ^[a-zA-Z0-9 .,'-]{1,100}$
Problem: None -- anything not matching the pattern is rejected

Validation in Practice

import re
from dataclasses import dataclass
from typing import Optional
# Validation utilities
def validate_email(email: str) -> str:
"""Validate email format using a strict pattern."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email) or len(email) > 254:
raise ValueError("Invalid email address")
return email.lower().strip()
def validate_username(username: str) -> str:
"""Validate username: alphanumeric, 3-30 chars."""
pattern = r'^[a-zA-Z0-9_-]{3,30}$'
if not re.match(pattern, username):
raise ValueError("Username must be 3-30 alphanumeric characters")
return username
def validate_age(age_input: str) -> int:
"""Validate age is a reasonable integer."""
try:
age = int(age_input)
except (ValueError, TypeError):
raise ValueError("Age must be a number")
if not (0 <= age <= 150):
raise ValueError("Age must be between 0 and 150")
return age
# Using schema validation (recommended for complex inputs)
from pydantic import BaseModel, validator, constr
class CreateUserRequest(BaseModel):
username: constr(min_length=3, max_length=30, pattern=r'^[a-zA-Z0-9_-]+$')
email: constr(max_length=254)
age: int
bio: Optional[constr(max_length=500)] = None
@validator("email")
def validate_email(cls, v):
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, v):
raise ValueError("Invalid email format")
return v.lower().strip()
@validator("age")
def validate_age(cls, v):
if not (0 <= v <= 150):
raise ValueError("Age must be between 0 and 150")
return v
# Usage in an endpoint
@app.route("/api/users", methods=["POST"])
def create_user():
try:
data = CreateUserRequest(**request.json)
except ValidationError as e:
abort(400, description=str(e))
# data is now validated and safe to use
user = User(username=data.username, email=data.email, age=data.age)
db.session.add(user)
db.session.commit()
return jsonify(user.to_dict()), 201

Parameterized Queries and ORM Usage

Never concatenate user input into SQL queries. Use parameterized queries or an ORM:

# BAD: String concatenation (SQL injection vulnerable)
query = f"SELECT * FROM users WHERE name = '{name}' AND age > {age}"
# GOOD: Parameterized query (raw SQL)
cursor.execute("SELECT * FROM users WHERE name = %s AND age > %s", (name, age))
# BEST: ORM (SQLAlchemy)
users = User.query.filter(User.name == name, User.age > age).all()

Output Encoding

Input validation prevents malicious data from entering your system. Output encoding prevents malicious data from being interpreted as code when rendered in a different context (HTML, JavaScript, URLs, SQL).

Context-Specific Encoding

ContextEncodingExample
HTML bodyHTML entity encoding<script> becomes &lt;script&gt;
HTML attributeHTML attribute encoding"onmouseover="alert(1)" becomes &quot;onmouseover=&quot;alert(1)&quot;
JavaScriptJavaScript encodingBackslash-escape special characters
URLURL/percent encoding<script> becomes %3Cscript%3E
CSSCSS hex encodingEscape special CSS characters

Best Practices

  • Use template engines that auto-escape output by default (Jinja2, React JSX, EJS with <%= %>)
  • Never use innerHTML or dangerouslySetInnerHTML with user data — use textContent instead
  • Encode for the correct context — HTML encoding in a JavaScript context does not prevent XSS
  • Use Content Security Policy (CSP) as an additional layer to block inline scripts

Secrets Management

Secrets — API keys, database passwords, encryption keys, service account credentials — are the keys to your kingdom. Mishandling them is one of the fastest paths to a catastrophic breach.

The Golden Rules

  1. NEVER hardcode secrets in source code — they end up in version control and are exposed to everyone with repository access
  2. NEVER commit secrets to version control — even if you delete them later, they remain in git history forever
  3. NEVER log secrets — log files are often stored insecurely and accessed broadly
  4. NEVER pass secrets via URL parameters — they appear in browser history, server logs, and referrer headers

Secrets Management Approaches (Least to Most Secure)

ApproachSecurityWhen to Use
Hardcoded in source codeNoneNever
.env file (gitignored)LowLocal development only
Environment variablesModerateSimple deployments, CI/CD
Encrypted config filesGoodWhen a vault is not available
Secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager)BestProduction systems

Practical Secrets Management

# BAD: Hardcoded secret
DATABASE_URL = "postgresql://admin:SuperSecret123@db.example.com/myapp"
API_KEY = "sk_live_abc123def456"
# GOOD: Environment variables (basic)
import os
DATABASE_URL = os.environ["DATABASE_URL"] # Raises KeyError if missing
API_KEY = os.environ.get("API_KEY") # Returns None if missing
# Fail fast if required secrets are not configured
required_vars = ["DATABASE_URL", "API_KEY", "JWT_SECRET"]
missing = [var for var in required_vars if var not in os.environ]
if missing:
raise RuntimeError(f"Missing required environment variables: {', '.join(missing)}")
# BEST: Using a secrets manager (AWS Secrets Manager example)
import boto3
import json
def get_secret(secret_name: str) -> dict:
"""Retrieve a secret from AWS Secrets Manager."""
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
# Secrets are fetched at startup, not hardcoded
db_credentials = get_secret("prod/myapp/database")
DATABASE_URL = (
f"postgresql://{db_credentials['username']}:{db_credentials['password']}"
f"@{db_credentials['host']}/{db_credentials['dbname']}"
)

Preventing Secrets from Leaking into Version Control

Terminal window
# .gitignore -- ALWAYS include these
.env
.env.local
.env.*.local
*.key
*.pem
credentials.json
service-account.json

Use pre-commit hooks to scan for accidentally committed secrets:

Terminal window
# Install git-secrets (AWS) or gitleaks
# gitleaks: https://github.com/gitleaks/gitleaks
gitleaks detect --source=. --verbose
# Or use pre-commit framework
# .pre-commit-config.yaml
# repos:
# - repo: https://github.com/gitleaks/gitleaks
# rev: v8.18.0
# hooks:
# - id: gitleaks

Dependency Security

Modern applications depend on hundreds of third-party packages, and each one is a potential entry point for attackers. A single vulnerable transitive dependency can compromise your entire application.

Software Composition Analysis (SCA) Tools

ToolLanguageHow to Run
npm auditJavaScript/Node.jsnpm audit (built-in)
yarn auditJavaScript/Node.jsyarn audit (built-in)
pip-auditPythonpip install pip-audit && pip-audit
cargo auditRustcargo install cargo-audit && cargo audit
bundler-auditRubygem install bundler-audit && bundler-audit
SnykMulti-languagesnyk test (free tier available)
DependabotMulti-languageBuilt into GitHub (enable in repository settings)
RenovateMulti-languageGitHub App or self-hosted

Dependency Security Best Practices

Terminal window
# 1. Audit current dependencies for known vulnerabilities
pip-audit
# 2. Pin exact versions in requirements.txt (reproducible builds)
pip freeze > requirements.txt
# Or use pip-compile for better dependency management
pip-compile requirements.in --generate-hashes
# 3. Example requirements.txt with pinned versions
# flask==3.0.0
# sqlalchemy==2.0.23
# bcrypt==4.1.2
# 4. Use a virtual environment to isolate dependencies
python -m venv venv
source venv/bin/activate
# 5. Regularly update and re-audit
pip install --upgrade flask sqlalchemy bcrypt
pip-audit

Automating Dependency Updates

Set up Dependabot (GitHub) or Renovate to automatically create pull requests when new versions are available:

.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "security-team"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"

Security Headers

HTTP security headers instruct browsers to enable built-in security features. Missing headers leave your application exposed to XSS, clickjacking, MIME sniffing, and protocol downgrade attacks.

Essential Security Headers

HeaderValuePurpose
Content-Security-Policydefault-src 'self'; script-src 'self'Prevents XSS by controlling which resources can load
Strict-Transport-Securitymax-age=63072000; includeSubDomains; preloadForces HTTPS, prevents protocol downgrade attacks
X-Frame-OptionsDENY or SAMEORIGINPrevents clickjacking by blocking iframe embedding
X-Content-Type-OptionsnosniffPrevents MIME type sniffing attacks
Referrer-Policystrict-origin-when-cross-originControls how much referrer info is sent with requests
Permissions-Policycamera=(), microphone=(), geolocation=()Restricts browser feature access

Implementing Security Headers

# Flask: Using a middleware/after_request handler
@app.after_request
def add_security_headers(response):
# Prevent XSS -- restrict resource loading to same origin
response.headers["Content-Security-Policy"] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data: https:; "
"font-src 'self'; "
"connect-src 'self'; "
"frame-ancestors 'none';"
)
# Force HTTPS for 2 years, include subdomains
response.headers["Strict-Transport-Security"] = (
"max-age=63072000; includeSubDomains; preload"
)
# Prevent clickjacking
response.headers["X-Frame-Options"] = "DENY"
# Prevent MIME type sniffing
response.headers["X-Content-Type-Options"] = "nosniff"
# Control referrer information
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
# Restrict browser features
response.headers["Permissions-Policy"] = (
"camera=(), microphone=(), geolocation=()"
)
return response

CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which domains can make requests to your API. Misconfigured CORS is a common security vulnerability.

# BAD: Allow all origins (disables CORS protection entirely)
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})
# GOOD: Restrict to specific trusted origins
CORS(app, resources={r"/api/*": {
"origins": [
"https://myapp.com",
"https://staging.myapp.com",
],
"methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Content-Type", "Authorization"],
"max_age": 3600,
"supports_credentials": True,
}})

Security Logging Best Practices

Effective security logging is the difference between detecting a breach in minutes and discovering it months later.

What to Log

Event CategorySpecific Events
AuthenticationLogin success, login failure, logout, password change, MFA enrollment/use
AuthorizationAccess denied (403), privilege escalation attempts, role changes
Data accessSensitive data reads, bulk exports, unusual query patterns
AdministrativeUser creation/deletion, permission changes, configuration changes
Input validationRejected inputs, malformed requests, oversized payloads
System eventsApplication start/stop, dependency failures, error spikes

What NOT to Log

Never LogWhy
Passwords (even failed attempts)Log files become a breach target
Full credit card numbersPCI DSS violation
Social Security Numbers / national IDsRegulatory violation (GDPR, HIPAA)
Session tokens or API keysAttacker with log access gains session hijacking capability
Personal health informationHIPAA violation
Encryption keys or secretsDefeats the purpose of encryption

Structured Logging Example

{
"timestamp": "2024-01-15T10:30:45.123Z",
"level": "warn",
"event": "authentication.failed",
"user_email": "user@example.com",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0...",
"reason": "invalid_password",
"attempt_count": 3,
"request_id": "req_abc123",
"geo": "US-CA"
}

SAST and DAST

Static and dynamic analysis tools catch vulnerabilities automatically at different stages of the development lifecycle.

Static Application Security Testing (SAST)

SAST tools analyze source code without executing it. They run early in the pipeline (on every commit or PR).

ToolLanguageNotes
BanditPythonFinds common security issues in Python code
ESLint security pluginsJavaScripteslint-plugin-security, eslint-plugin-no-unsanitized
SemgrepMulti-languageCustom rules, fast, open-source
SonarQubeMulti-languageComprehensive, includes code quality and security
CodeQLMulti-languageGitHub-native, powerful query language

Dynamic Application Security Testing (DAST)

DAST tools test a running application by sending crafted requests and analyzing responses.

ToolTypeNotes
OWASP ZAPOpen-sourceFull-featured web app scanner
Burp SuiteCommercialIndustry-standard for penetration testing
NucleiOpen-sourceTemplate-based vulnerability scanner

Integrating Security Testing into CI/CD

# Example GitHub Actions workflow
name: Security Checks
on: [push, pull_request]
jobs:
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten
- name: Run Bandit (Python)
run: |
pip install bandit
bandit -r src/ -f json -o bandit-report.json
- name: Run npm audit
run: npm audit --audit-level=high
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run pip-audit
run: |
pip install pip-audit
pip-audit
- name: Run Trivy (container scanning)
uses: aquasecurity/trivy-action@master
with:
scan-type: fs
severity: HIGH,CRITICAL

Secure Code Review Checklist

Use this checklist during code reviews to systematically check for security issues.

Input & Output

CheckQuestion
Input validationIs all user input validated using allowlists and length limits?
Parameterized queriesAre all database queries using parameterized statements or ORM?
Output encodingIs output encoded for the correct context (HTML, JS, URL)?
File uploadsAre file types validated, sizes limited, and files stored outside the web root?
DeserializationIs untrusted data ever deserialized using unsafe mechanisms (pickle, eval)?

Authentication & Authorization

CheckQuestion
Password storageAre passwords hashed with bcrypt or Argon2id (never MD5/SHA-1)?
Authorization checksIs authorization enforced on every endpoint, not just the UI?
Session managementDo sessions expire, and are tokens invalidated on logout?
Rate limitingAre authentication endpoints protected against brute force?
MFAIs multi-factor authentication available for sensitive operations?

Secrets & Configuration

CheckQuestion
No hardcoded secretsAre there any API keys, passwords, or tokens in the source code?
Environment configAre secrets loaded from environment variables or a secrets manager?
Debug modeIs debug mode disabled in production configuration?
Error messagesDo error responses avoid leaking stack traces or internal details?
Security headersAre CSP, HSTS, X-Frame-Options, and other headers configured?

Dependencies & Infrastructure

CheckQuestion
Dependency auditHas npm audit / pip-audit been run with no high/critical findings?
Lockfile committedIs the lockfile (package-lock.json, Pipfile.lock) committed?
Minimal permissionsDo service accounts and database users follow least privilege?
CORS configurationIs CORS restricted to specific trusted origins (not *)?
TLSIs HTTPS enforced with TLS 1.2+ and strong cipher suites?

Logging & Monitoring

CheckQuestion
Security events loggedAre authentication, authorization, and validation failures logged?
No sensitive data in logsAre passwords, tokens, PII, and secrets excluded from logs?
Structured loggingAre logs in a parseable format (JSON) with consistent fields?
AlertingAre alerts configured for suspicious patterns (brute force, data exfiltration)?

Comprehensive Secure Coding Checklist

This table summarizes all secure coding practices covered in this section. Use it as a quick reference during development and code reviews.

CategoryPracticePriority
InputValidate all input with allowlistsCritical
InputUse parameterized queries for all database operationsCritical
InputSet maximum lengths on all string inputsHigh
InputValidate file uploads (type, size, content)High
OutputEncode output for the correct context (HTML, JS, URL)Critical
OutputUse template engines with auto-escapingHigh
OutputImplement Content Security Policy (CSP)High
AuthHash passwords with bcrypt or Argon2idCritical
AuthImplement rate limiting on authentication endpointsCritical
AuthUse short-lived tokens with refresh token rotationHigh
AuthEnforce multi-factor authentication for sensitive operationsHigh
AuthInvalidate sessions on logout and password changeHigh
SecretsNever hardcode secrets in source codeCritical
SecretsUse a secrets manager in productionHigh
SecretsUse pre-commit hooks to detect accidental secret commitsHigh
SecretsRotate secrets regularlyMedium
DependenciesRun npm audit / pip-audit in CI/CDCritical
DependenciesEnable automated dependency updates (Dependabot/Renovate)High
DependenciesRemove unused dependenciesMedium
DependenciesCommit lockfiles for reproducible buildsHigh
HeadersSet HSTS with long max-age and includeSubDomainsCritical
HeadersConfigure CSP to restrict script and resource loadingHigh
HeadersSet X-Frame-Options, X-Content-Type-OptionsHigh
HeadersRestrict CORS to trusted originsHigh
LoggingLog all authentication and authorization eventsCritical
LoggingNever log passwords, tokens, or PIICritical
LoggingUse structured logging (JSON)High
LoggingSet up alerting for suspicious patternsHigh
TestingRun SAST tools (Semgrep, Bandit) in CI/CDHigh
TestingPerform regular DAST scans (ZAP, Burp)Medium
TestingConduct periodic security code reviewsHigh
TestingPerform annual penetration testingMedium

Next Steps