Comprehensive secure coding practices covering input validation, authentication, authorization, cryptography, secrets management, and error handling. Provides actionable code examples and checklists for building security into every stage of development.
Comprehensive secure coding practices covering input validation, authentication, authorization, cryptography, secrets management, and error handling. Provides actionable code examples and checklists for building security into every stage of development.
Writing code that is resistant to attack requires intentional practices at every layer. This skill covers the fundamental security patterns every developer should implement: input validation, authentication, authorization, cryptography, secrets management, and error handling.
Core Principles
1. Never Trust User Input
All input is guilty until proven innocent. Validate, sanitize, and parameterize every piece of data that crosses a trust boundary — HTTP requests, file uploads, database queries, API calls, message queue payloads.
2. Fail Securely
When something goes wrong, the default behavior should be denial, not access. An error should reject the request, log the event, and return minimal information to the user.
3. Defense in Depth
No single control is sufficient. If input validation fails, parameterized queries should prevent injection. If authentication is bypassed, authorization should block access. Layer your defenses.
4. Keep Security Simple
Complex cryptography, custom authentication schemes, and convoluted permission models are more likely to have bugs. Use well-vetted libraries. Do not roll your own crypto.
5. Least Privilege
Every component, function, and user should have the minimum permissions required to do its job. Apply this to database accounts, API keys, file permissions, and cloud IAM roles.
6. Secure by Default
Features should be secure out of the box. Developers should have to explicitly weaken security, not opt into it. Default deny, not default allow.
7. Don't Expose Internals
Error messages, stack traces, debug endpoints, and internal IPs should never reach the client. What the user doesn't know, they can't exploit.
Secure Coding Checklist
Use this checklist during code review and development:
Pre-Commit
All user input validated (type, length, format, range)
SQL queries use parameterized statements (no string concatenation)
No secrets in source code (API keys, passwords, tokens)
Authentication tokens are not logged
Output is encoded for the target context (HTML, JS, CSS, URL)
Input validation is the first line of defense against injection, XSS, and data corruption attacks.
Allowlist vs. Denylist
Approach
Strategy
Example
Recommendation
Allowlist
Only permit known-good input
^[a-zA-Z0-9_]+$
✅ Always preferred
Denylist
Block known-bad input
Block ', ", ;, --
❌ Easy to bypass
Why allowlist wins: Denylists are incomplete. Attackers constantly find new payloads. An allowlist says "you can only send what we expect" — everything else is rejected.
# ❌ Denylist (bad)defvalidate_username(username):
forbidden = ["'", "\"", ";", "--", "DROP", "SELECT"]
for char in forbidden:
if char in username:
raise ValueError("Invalid characters")
return username # Still vulnerable to edge cases# ✅ Allowlist (good)import re
defvalidate_username(username):
ifnot re.match(r'^[a-zA-Z0-9_]{3,32}$', username):
raise ValueError("Username must be 3-32 alphanumeric characters or underscores")
return username
Sanitization
Sanitize (clean) data when you must accept a broader range of input but still need to prevent injection.
✅ Rotate session ID on login and privilege escalation.
✅ Provide a logout mechanism that destroys the server-side session.
Authorization
RBAC (Role-Based Access Control)
from functools import wraps
ROLES = {
"admin": ["read", "write", "delete", "manage_users"],
"editor": ["read", "write"],
"viewer": ["read"],
}
defrequire_permission(permission):
defdecorator(func):
@wraps(func)defwrapper(*args, **kwargs):
user_role = get_current_user().role
if permission notin ROLES.get(user_role, []):
raise PermissionError("Insufficient permissions")
return func(*args, **kwargs)
return wrapper
return decorator
# Usage@require_permission("delete")defdelete_post(post_id):
# Only admins can reach here
Post.delete(post_id)
ABAC (Attribute-Based Access Control)
ABAC evaluates policies based on user attributes, resource attributes, and environment attributes.
# Policy: A user can view a document if:# - They are the owner, OR# - They are in the same department AND document access level <= user's clearancedefcan_access_document(user, document):
# Attribute checksif user.id == document.owner_id:
returnTrueif user.department == document.department:
if document.classification == "public":
returnTrueif document.classification == "internal"and user.clearance >= 1:
returnTrueif document.classification == "confidential"and user.clearance >= 2:
returnTruereturnFalse
Principle of Least Privilege
# ❌ Over-privileged service account
database_user = "app_admin"# Has full DDL/DML access# If compromised, attacker can drop tables# ✅ Least privilege service account
database_user = "app_worker"# Only has SELECT, INSERT, UPDATE on specific tables# GRANT SELECT, INSERT, UPDATE ON app.users TO 'app_worker';# GRANT SELECT, INSERT ON app.orders TO 'app_worker';# No DELETE, no DROP, no access to other databases# ❌ Full admin API key
api_key = "sk-XXXXXXXXXXXXXXXX"# Full access, no restrictions# ✅ Scoped API key# Create with restricted scope: only read access to users endpoint# Rate limited, IP-restricted, no billing access
Output Encoding (XSS Prevention)
Context-aware encoding prevents Cross-Site Scripting (XSS) by ensuring user data is treated as data, not code.
# config.pyimport os
classConfig:
SECRET_KEY = os.environ.get("SECRET_KEY")
DATABASE_URL = os.environ.get("DATABASE_URL")
classDevelopmentConfig(Config):
DEBUG = True
DATABASE_URL = os.environ.get("DEV_DATABASE_URL", "sqlite:///dev.db")
classProductionConfig(Config):
DEBUG = False# All secrets MUST come from environment or secrets managerassert Config.SECRET_KEY isnotNone, "SECRET_KEY must be set in production"
Error Handling (Information Leakage)
What NOT to Show to Users
# ❌ Bad: Exposes internalstry:
result = process_payment(card_number, amount)
except Exception as e:
return {"error": f"Payment failed: {str(e)} at line {e.__traceback__.tb_lineno}"}
# Output: "Payment failed: 'NoneType' object has no attribute 'balance' at line 42"# ✅ Good: Generic user-facing error, detailed server-side logimport logging
logger = logging.getLogger(__name__)
try:
result = process_payment(card_number, amount)
except ValueError as e:
# Expected error (invalid card) - give specific but safe messagereturn {"error": "Payment declined. Please check your card details."}
except Exception as e:
# Unexpected error - generic message, log details
logger.error(f"Payment processing failed: {e}", exc_info=True)
return {"error": "An unexpected error occurred. Our team has been notified."}
Don't Leak User Existence
# ❌ Bad: Leaks whether a user existsif user_exists(email):
return {"error": "User already registered"}
else:
# Send verification emailreturn {"message": "Verification email sent"}
# ✅ Good: Consistent response regardless# Always say "If the email exists, a verification link was sent"defregister(email):
# Same response regardless of outcome
send_verification_email(email) # Only sends if email not already verifiedreturn {"message": "If the account exists, a verification link has been sent."}
# Same for login:deflogin(email, password):
# Don't say "user not found" vs "wrong password"
user = get_user_by_email(email)
ifnot user ornot verify_password(password, user.password_hash):
return {"error": "Invalid email or password"} # 🔒 Ambiguousreturn {"token": create_session(user)}
HTTP Status Code Hygiene
# ❌ Bad: Leaks information via status codes# 200: Login successful# 401: Wrong password# 404: User not found# ✅ Good: Consistent responses# Always return 401 for failed authentication
LOGIN_FAILURE_RESPONSE = ({"error": "Invalid credentials"}, 401)
Common Mistakes
1. Client-Side Authorization Only
Checking roles in JavaScript doesn't prevent attackers from calling APIs directly. All authorization must be enforced server-side.
2. Using eval() or Dynamically Executing User Input
eval(request.body.expression) is a remote code execution vulnerability. Same for exec(), os.system(), and template engines with user-controlled templates.
3. Rolling Your Own Cryptography
Custom encryption algorithms, homemade password hashing schemes, and "I'll just XOR it" are guaranteed to be broken. Use standard libraries.
4. Ignoring Type Confusion
Accepting {"id": 5} when you expected a string, or {"role": "admin"} when the field should be read-only. Validate types and don't blindly deserialize user input into objects.
5. Logging Sensitive Data
Passwords, credit card numbers, API keys, and session tokens should never appear in logs. Use structured logging with sensitive field redaction.
6. No Rate Limiting on Auth Endpoints
Without rate limiting, an attacker can brute-force passwords, enumerate users, or exhaust server resources. Implement rate limiting on login, registration, and password reset endpoints.
7. Trusting Uploaded File Names
An attacker can upload ../../../etc/passwd or malware.exe as the filename. Never use the user-supplied filename without sanitization. Generate your own filenames serverside.
8. Skipping Input Validation on Internal APIs
"Internal" doesn't mean "safe." Internal services, microservices, and admin APIs must validate input just as strictly as public endpoints.
9. Storing Passwords in Plaintext
Despite decades of warnings, this still happens. Use bcrypt, argon2, or scrypt. There is no excuse for hashing with MD5 or storing plaintext.
10. Ignoring Dependency Vulnerabilities
A single outdated library can compromise your entire application. Run npm audit, pip-audit, or Trivy regularly. Subscribe to CVE alerts for your stack.
11. Hardcoded Secrets in CI/CD
CI/CD pipelines often contain tokens, API keys, or service account credentials. Store these in CI/CD secrets (GitHub Actions secrets, Jenkins credentials) — never in the .yml file.
12. Not Handling Race Conditions
Two concurrent requests can bypass a single check. Use database transactions, atomic operations, or distributed locks. Check-then-act patterns are vulnerable to TOCTOU (Time of Check, Time of Use).
13. JWT Algorithm Confusion
An attacker modifies the JWT header from RS256 to HS256 and signs it with the public key (which is... public). Always validate the algorithm server-side and reject unexpected algorithms.
14. Forgetting About CSRF
If your API uses cookie-based authentication, you need CSRF tokens or SameSite cookies. A user visiting evil.com should not be able to make requests to your API with the user's credentials.
15. Treating Security as an Afterthought
Security added post-development is more expensive, less effective, and harder to audit. Build security into the design from day one.