Secure Coding Standards
Secure coding standards define the security controls that developers implement directly in application code. This reference provides lookup tables for validation rules, encoding requirements, cryptographic parameters, and vulnerability mitigations organised by security domain.
Input Validation
Input validation rejects data that fails to meet defined constraints before the application processes it. All input originates from untrusted sources regardless of apparent origin, including form fields, URL parameters, HTTP headers, cookies, file uploads, API payloads, and data retrieved from databases or external services.
Validation Approach
Validation uses an allowlist model that defines what constitutes valid input and rejects everything else. Blocklist approaches that attempt to filter known-bad patterns fail against novel attack variations and encoding tricks.
# Allowlist validation - correct approachimport re
def validate_username(username: str) -> bool: """Username must be 3-20 alphanumeric characters or underscores.""" pattern = r'^[a-zA-Z0-9_]{3,20}$' return bool(re.match(pattern, username))
# Blocklist validation - incorrect approachdef validate_username_blocklist(username: str) -> bool: """Attempts to block dangerous characters - will miss edge cases.""" dangerous = ['<', '>', '"', "'", ';', '--'] return not any(char in username for char in dangerous)Validation occurs at the boundary where data enters the application, not deeper in the processing chain. Data that passes validation at entry remains validated for that context but requires revalidation if used in a different context, such as data validated for display being subsequently used in a SQL query.
Validation Rules by Data Type
| Data type | Validation rule | Maximum length | Character set |
|---|---|---|---|
| Username | Alphanumeric plus underscore | 20 | [a-zA-Z0-9_] |
| Email address | RFC 5322 format, verified domain | 254 | Per RFC 5322 |
| Password | Minimum 12 characters, no maximum | 128 (for hashing) | Any Unicode |
| Phone number | E.164 format after normalisation | 15 | [0-9+] |
| Postal code | Country-specific pattern | 10 | [a-zA-Z0-9 -] |
| URL | Valid scheme (https preferred), valid host | 2048 | Per RFC 3986 |
| UUID | RFC 4122 format | 36 | [a-fA-F0-9-] |
| Integer ID | Numeric, within expected range | 19 digits | [0-9] |
| Date | ISO 8601 format, reasonable range | 10 | [0-9-] |
| Currency amount | Decimal with 2 places, positive or zero | 15 digits | [0-9.] |
| Free text (single line) | No control characters | 500 | Printable Unicode |
| Free text (multi-line) | No dangerous control characters | 10000 | Printable Unicode plus newline |
| File name | No path separators, no null bytes | 255 | [a-zA-Z0-9._-] |
| JSON payload | Valid JSON, schema validation | Context-dependent | Valid JSON |
| HTML content | Never accept; require structured input | N/A | N/A |
Numeric Validation
Numeric inputs require range validation in addition to format validation. An integer that parses correctly but exceeds expected bounds causes application errors or enables integer overflow attacks.
def validate_quantity(value: str) -> int: """Validate and parse a quantity field.""" try: quantity = int(value) except ValueError: raise ValidationError("Quantity must be a whole number")
if quantity < 1: raise ValidationError("Quantity must be at least 1") if quantity > 10000: raise ValidationError("Quantity cannot exceed 10000")
return quantity| Numeric field | Minimum | Maximum | Precision |
|---|---|---|---|
| Quantity | 1 | 10,000 | Integer |
| Currency | 0.00 | 999,999,999.99 | 2 decimal places |
| Percentage | 0 | 100 | 2 decimal places |
| Year | 1900 | 2100 | Integer |
| Age | 0 | 150 | Integer |
| Port number | 1 | 65535 | Integer |
| Page number | 1 | 10,000 | Integer |
| Items per page | 1 | 100 | Integer |
File Upload Validation
File uploads require validation of the file extension, MIME type, file content (magic bytes), and file size. Relying solely on the extension or client-provided MIME type permits attackers to upload executable files disguised as images.
| Check | Method | Rationale |
|---|---|---|
| Extension | Extract from filename, compare to allowlist | First-pass filter; easily bypassed alone |
| MIME type | Read Content-Type header, compare to allowlist | Client-controlled; cannot be trusted alone |
| Magic bytes | Read first bytes, compare to known signatures | Verifies actual file format |
| File size | Check Content-Length and actual bytes read | Prevents resource exhaustion |
| Image dimensions | Parse image header, verify reasonable bounds | Prevents decompression bombs |
| Filename | Remove path components, sanitise characters | Prevents path traversal |
import magic
ALLOWED_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.pdf'}ALLOWED_MIMES = {'image/jpeg', 'image/png', 'application/pdf'}MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
def validate_upload(file) -> None: """Validate an uploaded file.""" # Check extension ext = Path(file.filename).suffix.lower() if ext not in ALLOWED_EXTENSIONS: raise ValidationError(f"File type {ext} not permitted")
# Check file size file.seek(0, 2) # Seek to end size = file.tell() file.seek(0) # Reset to beginning if size > MAX_FILE_SIZE: raise ValidationError("File exceeds 10 MB limit")
# Check magic bytes mime = magic.from_buffer(file.read(2048), mime=True) file.seek(0) if mime not in ALLOWED_MIMES: raise ValidationError(f"File content type {mime} not permitted")Output Encoding
Output encoding transforms data to prevent interpretation as code or markup in the output context. Data safe in one context becomes dangerous in another; a string containing <script> is harmless in a JSON response but executes as JavaScript when inserted into HTML without encoding.
Encoding by Context
| Output context | Encoding method | Characters encoded | Example |
|---|---|---|---|
| HTML body | HTML entity encoding | < > & " ' | < becomes < |
| HTML attribute | HTML entity encoding | < > & " ' ` | " becomes " |
| JavaScript string | JavaScript escape | \ ' " / < > and control chars | ' becomes \' |
| JavaScript in HTML | JavaScript escape then HTML encode | Both sets | Double encoding required |
| URL parameter | Percent encoding | Non-alphanumeric except - _ . ~ | space becomes %20 |
| CSS value | CSS escape | Non-alphanumeric | \ prefix with hex code |
| JSON value | JSON string escape | \ " / and control chars | " becomes \" |
| SQL (when parameterisation impossible) | Database-specific escape | ' \ NUL | Varies by database |
| LDAP | LDAP escape | \ * ( ) NUL | * becomes \2a |
| XML | XML entity encoding | < > & " ' | & becomes & |
| Command line | Shell escape or avoid entirely | Shell metacharacters | Prefer library calls |
Context-Specific Examples
HTML body encoding prevents injected markup from rendering as HTML elements:
from markupsafe import escape
def render_user_comment(comment: str) -> str: """Render a user comment safely in HTML.""" # escape() converts < > & " ' to HTML entities safe_comment = escape(comment) return f'<p class="comment">{safe_comment}</p>'
# Input: <script>alert('xss')</script># Output: <p class="comment"><script>alert('xss')</script></p>HTML attribute encoding handles the additional context of being inside a quoted attribute value:
<!-- Dangerous: unencoded user input in attribute --><a href="/search?q={{ user_input }}">Search</a>
<!-- Safe: URL-encoded user input --><a href="/search?q={{ user_input | urlencode }}">Search</a>JavaScript context requires JavaScript escaping, and JavaScript embedded in HTML requires both JavaScript escaping and HTML encoding:
import json
def render_config_script(user_preferences: dict) -> str: """Render user preferences as JavaScript configuration.""" # json.dumps handles JavaScript string escaping # The output is safe for a <script> block json_safe = json.dumps(user_preferences) return f'<script>const config = {json_safe};</script>'Encoding Functions by Language
| Language | HTML encoding | URL encoding | JSON encoding |
|---|---|---|---|
| Python | markupsafe.escape() | urllib.parse.quote() | json.dumps() |
| JavaScript | textContent property or library | encodeURIComponent() | JSON.stringify() |
| Java | StringEscapeUtils.escapeHtml4() | URLEncoder.encode() | Jackson/Gson |
| C# | HttpUtility.HtmlEncode() | Uri.EscapeDataString() | JsonSerializer.Serialize() |
| PHP | htmlspecialchars() | urlencode() | json_encode() |
| Ruby | ERB::Util.html_escape() | CGI.escape() | JSON.generate() |
| Go | html.EscapeString() | url.QueryEscape() | json.Marshal() |
SQL Injection Prevention
SQL injection occurs when attacker-controlled input becomes part of a SQL query structure, allowing execution of arbitrary SQL commands. Prevention requires separating query structure from query data through parameterised queries.
Parameterised Queries
Parameterised queries (also called prepared statements) send query structure and data separately to the database. The database parses the query structure first, then binds the data values, making it impossible for data to alter query structure.
# Vulnerable: string concatenationdef get_user_vulnerable(username: str): query = f"SELECT * FROM users WHERE username = '{username}'" cursor.execute(query) # Input: admin'-- # Resulting query: SELECT * FROM users WHERE username = 'admin'--' # The -- comments out the rest, bypassing any additional conditions
# Secure: parameterised querydef get_user_secure(username: str): query = "SELECT * FROM users WHERE username = %s" cursor.execute(query, (username,)) # The database treats username as data, never as SQLParameter Syntax by Database
| Database | Positional parameter | Named parameter |
|---|---|---|
| PostgreSQL | $1, $2 | Not supported |
| MySQL | %s | %(name)s |
| SQLite | ? | :name |
| SQL Server | @p1 | @name |
| Oracle | :1 | :name |
ORM Usage
Object-Relational Mappers (ORMs) generate parameterised queries when used correctly. Raw query methods bypass this protection and require the same care as direct SQL.
# Django ORM - safe by defaultUser.objects.filter(username=user_input)
# Django ORM - raw query requires parameterisationUser.objects.raw('SELECT * FROM users WHERE username = %s', [user_input])
# SQLAlchemy - safe by defaultsession.query(User).filter(User.username == user_input)
# SQLAlchemy - text queries require parametersfrom sqlalchemy import textsession.execute(text('SELECT * FROM users WHERE username = :name'), {'name': user_input})Dynamic Query Elements
Identifiers (table names, column names) and SQL keywords cannot be parameterised. When queries require dynamic identifiers, validate against an explicit allowlist.
ALLOWED_SORT_COLUMNS = {'username', 'created_at', 'email'}ALLOWED_SORT_DIRECTIONS = {'ASC', 'DESC'}
def get_users_sorted(sort_column: str, sort_direction: str): # Validate against allowlist - never use user input directly if sort_column not in ALLOWED_SORT_COLUMNS: raise ValidationError("Invalid sort column") if sort_direction.upper() not in ALLOWED_SORT_DIRECTIONS: raise ValidationError("Invalid sort direction")
# Safe to include validated identifiers query = f"SELECT * FROM users ORDER BY {sort_column} {sort_direction}" cursor.execute(query)Cross-Site Scripting Prevention
Cross-site scripting (XSS) injects malicious scripts into pages viewed by other users. Prevention combines input validation, output encoding, and Content Security Policy headers.
XSS Types and Mitigations
Stored XSS persists malicious input in the database, affecting all users who view the stored content. Reflected XSS includes malicious input in the immediate response, affecting users who click crafted links. DOM-based XSS occurs when client-side JavaScript insecurely handles data.
| XSS type | Attack vector | Primary mitigation | Secondary mitigation |
|---|---|---|---|
| Stored | Database content rendered without encoding | Output encoding | Input validation, CSP |
| Reflected | URL parameter rendered without encoding | Output encoding | Input validation, CSP |
| DOM-based | JavaScript handling of location, document properties | Safe DOM APIs | CSP, Trusted Types |
Safe DOM APIs
JavaScript DOM manipulation must use APIs that treat data as text, not as HTML.
| Dangerous API | Safe alternative | Reason |
|---|---|---|
innerHTML | textContent | innerHTML parses as HTML |
outerHTML | textContent on parent | outerHTML parses as HTML |
document.write() | DOM manipulation methods | Writes directly to document |
eval() | JSON.parse() for JSON | eval executes arbitrary code |
setTimeout(string) | setTimeout(function) | String form uses eval |
setInterval(string) | setInterval(function) | String form uses eval |
// Dangerous: innerHTML interprets HTMLelement.innerHTML = userInput;
// Safe: textContent treats input as textelement.textContent = userInput;
// When HTML structure is needed, create elements programmaticallyconst link = document.createElement('a');link.href = sanitisedUrl;link.textContent = userInput; // User input only in text contentparentElement.appendChild(link);Content Security Policy
Content Security Policy (CSP) HTTP headers instruct browsers to restrict resource loading and script execution. CSP provides defence in depth when encoding mistakes occur.
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' https://api.example.org; frame-ancestors 'none'; base-uri 'self'; form-action 'self';| Directive | Recommended value | Effect |
|---|---|---|
default-src | 'self' | Fallback for unspecified directives |
script-src | 'self' | Blocks inline scripts and external script sources |
style-src | 'self' | Blocks inline styles (use 'unsafe-inline' if required) |
img-src | 'self' data: https: | Allows images from same origin, data URIs, HTTPS |
connect-src | 'self' plus API domains | Restricts fetch/XHR destinations |
frame-ancestors | 'none' | Prevents framing (clickjacking protection) |
base-uri | 'self' | Prevents base tag injection |
form-action | 'self' | Restricts form submission targets |
Cross-Site Request Forgery Prevention
Cross-site request forgery (CSRF) tricks authenticated users into submitting requests to applications where they have active sessions. Prevention requires verifying that requests originate from the legitimate application.
CSRF Token Implementation
CSRF tokens are random values generated server-side, embedded in forms, and validated on submission. The token proves the request originated from a page served by the application.
import secrets
def generate_csrf_token(session) -> str: """Generate and store a CSRF token for the session.""" token = secrets.token_urlsafe(32) session['csrf_token'] = token return token
def validate_csrf_token(session, submitted_token: str) -> bool: """Validate the submitted CSRF token against the session token.""" stored_token = session.get('csrf_token') if not stored_token: return False return secrets.compare_digest(stored_token, submitted_token)<form method="POST" action="/transfer"> <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> <input type="text" name="amount"> <button type="submit">Transfer</button></form>Additional CSRF Defences
| Defence | Mechanism | Limitations |
|---|---|---|
| CSRF token | Random token validated server-side | Requires server-side session |
| SameSite cookies | Cookie attribute restricts cross-site sending | Browser support varies; not all request types |
| Origin header check | Verify Origin or Referer header matches | Headers can be absent in some scenarios |
| Custom request header | Require X-Requested-With or similar | Only works for AJAX requests |
| Re-authentication | Require password for sensitive actions | User friction |
SameSite cookie attribute configuration:
# Session cookie configurationresponse.set_cookie( 'session_id', value=session_id, secure=True, # HTTPS only httponly=True, # Not accessible to JavaScript samesite='Lax' # Sent with top-level navigations, not cross-site requests)| SameSite value | Behaviour |
|---|---|
Strict | Never sent with cross-site requests |
Lax | Sent with top-level navigations (links) but not embedded requests |
None | Sent with all requests (requires Secure attribute) |
Authentication Implementation
Authentication implementation must use established libraries and frameworks. Custom authentication code introduces vulnerabilities. This section specifies requirements for authentication controls.
Password Handling
Passwords are hashed using memory-hard algorithms designed for password storage. General-purpose cryptographic hashes (SHA-256, MD5) are unsuitable because they are fast to compute, enabling rapid brute-force attacks.
| Algorithm | Status | Parameters |
|---|---|---|
| Argon2id | Recommended | Memory: 64 MB, iterations: 3, parallelism: 4 |
| bcrypt | Acceptable | Cost factor: 12 minimum |
| scrypt | Acceptable | N: 2^17, r: 8, p: 1 |
| PBKDF2-SHA256 | Legacy only | 600,000 iterations minimum |
| SHA-256, SHA-512 | Prohibited | N/A |
| MD5 | Prohibited | N/A |
from argon2 import PasswordHasher
ph = PasswordHasher( time_cost=3, # Iterations memory_cost=65536, # 64 MB parallelism=4, hash_len=32, salt_len=16)
def hash_password(password: str) -> str: """Hash a password for storage.""" return ph.hash(password)
def verify_password(stored_hash: str, password: str) -> bool: """Verify a password against its hash.""" try: ph.verify(stored_hash, password) return True except argon2.exceptions.VerifyMismatchError: return FalseSession Management
| Requirement | Specification |
|---|---|
| Session ID length | 128 bits minimum (32 hex characters) |
| Session ID generation | Cryptographically secure random generator |
| Session ID transmission | Cookies only; never in URLs |
| Session ID rotation | Regenerate after authentication |
| Session timeout (idle) | 30 minutes for standard applications |
| Session timeout (absolute) | 12 hours maximum |
| Session invalidation | Server-side invalidation on logout |
import secrets
def create_session() -> str: """Create a new session with a secure random ID.""" session_id = secrets.token_hex(16) # 128 bits return session_id
def post_authentication(old_session_id: str, user_id: int) -> str: """Regenerate session ID after successful authentication.""" # Invalidate old session invalidate_session(old_session_id)
# Create new session new_session_id = create_session() associate_session_with_user(new_session_id, user_id)
return new_session_idMulti-Factor Authentication
MFA implementation requirements:
| Requirement | Specification |
|---|---|
| TOTP secret length | 160 bits minimum |
| TOTP time step | 30 seconds |
| TOTP valid window | Current period plus one previous period |
| Backup codes | 10 codes, single use, 8 characters each |
| Rate limiting | 5 failed attempts triggers 15-minute lockout |
import pyotp
def generate_totp_secret() -> str: """Generate a TOTP secret for a user.""" return pyotp.random_base32(length=32) # 160 bits
def verify_totp(secret: str, code: str) -> bool: """Verify a TOTP code with one-period tolerance.""" totp = pyotp.TOTP(secret) return totp.verify(code, valid_window=1)Authorisation Implementation
Authorisation determines whether an authenticated user has permission to access a resource or perform an action. Authorisation checks occur on every request, enforced server-side.
Authorisation Check Placement
+------------------------------------------------------------------+| REQUEST FLOW |+------------------------------------------------------------------+| || +----------+ +---------------+ +------------------+ || | | | | | | || | Client +---->+ Authentication+---->+ Authorisation | || | | | Middleware | | Middleware | || +----------+ +-------+-------+ +--------+---------+ || | | || v v || +-------+-------+ +--------+---------+ || | Is user | | Does user have | || | authenticated?| | permission for | || +-------+-------+ | this resource? | || | +--------+---------+ || | | || +----------+----------+ | || | | | || v v v || +----+----+ +-----+----+ +----+----+ || | No | | No | | Yes | || | 401 | | 403 | | Allow | || +---------+ +----------+ +---------+ || |+------------------------------------------------------------------+Common Authorisation Vulnerabilities
| Vulnerability | Description | Prevention |
|---|---|---|
| Insecure direct object reference | Accessing resources by guessing IDs | Verify ownership/permission for every resource access |
| Missing function-level access control | Administrative functions accessible to regular users | Check permissions at every endpoint |
| Path traversal | Accessing files outside intended directory | Validate paths, use allowlists, sandbox file access |
| Privilege escalation | User gains higher privileges | Validate role changes server-side |
| Parameter tampering | Modifying hidden form fields or API parameters | Ignore client-supplied authorisation data |
Resource-Level Authorisation
Every access to user-specific resources must verify the authenticated user has permission to access that specific resource, not merely that they are authenticated.
def get_document(document_id: int, current_user: User) -> Document: """Retrieve a document with ownership verification.""" document = Document.query.get(document_id)
if document is None: raise NotFoundError("Document not found")
# Verify ownership - not just authentication if document.owner_id != current_user.id: # Return 404 to avoid revealing document existence raise NotFoundError("Document not found")
return documentCryptography Standards
Cryptographic operations use approved algorithms, key lengths, and implementations. Custom cryptographic implementations are prohibited; use established libraries.
Approved Algorithms
| Purpose | Approved algorithm | Key/output size | Notes |
|---|---|---|---|
| Symmetric encryption | AES-256-GCM | 256-bit key | Authenticated encryption |
| Symmetric encryption | ChaCha20-Poly1305 | 256-bit key | Alternative to AES |
| Asymmetric encryption | RSA-OAEP | 2048-bit minimum, 4096-bit recommended | Use SHA-256 for OAEP |
| Digital signatures | RSA-PSS | 2048-bit minimum, 4096-bit recommended | Use SHA-256 |
| Digital signatures | Ed25519 | 256-bit | Preferred for new systems |
| Digital signatures | ECDSA P-256 | 256-bit | Acceptable |
| Key exchange | X25519 | 256-bit | Preferred |
| Key exchange | ECDH P-256 | 256-bit | Acceptable |
| Password hashing | Argon2id | Per parameters above | Memory-hard |
| General hashing | SHA-256 | 256-bit | Not for passwords |
| General hashing | SHA-384/512 | 384/512-bit | When longer output needed |
| HMAC | HMAC-SHA-256 | 256-bit key minimum | Message authentication |
Prohibited Algorithms
| Algorithm | Reason |
|---|---|
| DES | 56-bit key; trivially breakable |
| 3DES | Slow; 112-bit effective security |
| RC4 | Multiple practical attacks |
| MD5 | Collision attacks |
| SHA-1 | Collision attacks demonstrated |
| RSA PKCS#1 v1.5 | Padding oracle attacks |
| ECB mode | Reveals patterns in ciphertext |
| CBC without HMAC | Padding oracle attacks |
Secure Random Generation
Cryptographic random values require cryptographically secure pseudo-random number generators (CSPRNGs). Standard random number generators are predictable.
| Language | Secure random function | Insecure (never use) |
|---|---|---|
| Python | secrets module | random module |
| JavaScript | crypto.getRandomValues() | Math.random() |
| Java | SecureRandom | Random |
| C# | RandomNumberGenerator | Random |
| Go | crypto/rand | math/rand |
| PHP | random_bytes() | rand(), mt_rand() |
import secrets
# Generate secure random tokentoken = secrets.token_urlsafe(32)
# Generate secure random integer in rangerandom_id = secrets.randbelow(1000000)
# Secure comparison (timing-safe)secrets.compare_digest(value_a, value_b)Secrets in Code
Secrets include API keys, database credentials, encryption keys, tokens, and certificates. Secrets never appear in source code, version control history, logs, error messages, or client-side code.
Prohibited Practices
| Practice | Risk | Example |
|---|---|---|
| Hardcoded credentials | Exposed in source, version history | password = "admin123" |
| Credentials in config files (committed) | Same as hardcoded | config.py with DB_PASSWORD |
| Credentials in environment files (committed) | Same as hardcoded | .env checked into Git |
| Credentials in logs | Exposed in log aggregation | logger.info(f"Connecting with {password}") |
| Credentials in URLs | Exposed in browser history, logs | https://user:pass@host/ |
| Credentials in client-side code | Exposed to all users | API key in JavaScript |
Secret Management Approaches
| Approach | Suitability | Example |
|---|---|---|
| Environment variables | Development, simple deployments | os.environ['DATABASE_URL'] |
| Secret management service | Production | HashiCorp Vault, AWS Secrets Manager |
| Kubernetes secrets | Kubernetes deployments | Mounted as files or env vars |
| CI/CD secrets | Build pipelines | GitHub Actions secrets, GitLab CI variables |
import os
# Retrieve from environmentdatabase_url = os.environ.get('DATABASE_URL')if not database_url: raise ConfigurationError("DATABASE_URL environment variable required")
# Never log secretslogger.info("Connecting to database") # Correctlogger.info(f"Connecting to {database_url}") # Wrong - logs secretGit Secret Prevention
Configure Git to prevent accidental secret commits:
# .gitignore - exclude secret-containing files.env.env.**.pem*.keysecrets/config/local.pyPre-commit hooks detect secrets before they enter version control:
repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.0 hooks: - id: gitleaksError Handling and Logging
Error messages displayed to users and recorded in logs must not reveal sensitive information that assists attackers.
Error Message Content
| Information | Display to user | Include in logs |
|---|---|---|
| User-friendly error description | Yes | Yes |
| Error reference code | Yes | Yes |
| Stack trace | No | Yes (internal errors) |
| Database query | No | No (may contain data) |
| File paths | No | Yes |
| Internal IP addresses | No | Yes |
| Credentials | No | No |
| SQL error details | No | Yes |
| User input that caused error | No | Yes (sanitised) |
import uuidimport logging
logger = logging.getLogger(__name__)
def handle_database_error(error: Exception, context: dict) -> str: """Handle database error securely.""" error_id = str(uuid.uuid4())[:8]
# Log full details for debugging logger.error( "Database error", extra={ 'error_id': error_id, 'error_type': type(error).__name__, 'error_message': str(error), 'context': context } )
# Return safe message to user return f"A database error occurred. Reference: {error_id}"Logging Security Events
Security-relevant events require logging for audit and incident response. Each log entry includes timestamp, event type, user identity, resource affected, and outcome.
| Event | Log level | Required fields |
|---|---|---|
| Authentication success | INFO | User ID, IP address, method |
| Authentication failure | WARNING | Username attempted, IP address, reason |
| Authorisation failure | WARNING | User ID, resource, action attempted |
| Password change | INFO | User ID |
| Permission change | INFO | User ID, old permissions, new permissions, changed by |
| Data export | INFO | User ID, data type, record count |
| Administrative action | INFO | Admin user ID, action, target |
| Input validation failure | WARNING | IP address, field, reason (not the input itself) |
| Rate limit triggered | WARNING | IP address, endpoint, threshold |
logger.info( "Authentication successful", extra={ 'event_type': 'auth_success', 'user_id': user.id, 'ip_address': request.remote_addr, 'method': 'password' })
logger.warning( "Authentication failed", extra={ 'event_type': 'auth_failure', 'username_attempted': username, # Log username, not password 'ip_address': request.remote_addr, 'reason': 'invalid_password' })Dependency Security
Third-party dependencies introduce security vulnerabilities. Dependency management requires tracking, updating, and auditing external code.
Dependency Requirements
| Requirement | Specification |
|---|---|
| Version pinning | Pin exact versions in production |
| Lock files | Commit lock files to version control |
| Vulnerability scanning | Automated scanning in CI pipeline |
| Update frequency | Security patches within 72 hours; regular updates monthly |
| Licence compliance | Verify compatible licences before adoption |
| Source verification | Use official package repositories |
Vulnerability Response Times
| Severity | CVSS score | Response time |
|---|---|---|
| Critical | 9.0-10.0 | 72 hours |
| High | 7.0-8.9 | 1 week |
| Medium | 4.0-6.9 | 1 month |
| Low | 0.1-3.9 | Next regular update |
# Python - generate and audit dependenciespip freeze > requirements.txtpip-audit --requirement requirements.txt
# JavaScript - audit dependenciesnpm auditnpm audit fix
# Ruby - audit dependenciesbundle audit check --updateSecure Defaults
Applications ship with secure default configurations. Users may weaken security deliberately but must not face insecure configurations by default.
| Setting | Secure default | Insecure alternative |
|---|---|---|
| Cookie Secure flag | Enabled | Disabled |
| Cookie HttpOnly flag | Enabled | Disabled |
| Cookie SameSite | Lax or Strict | None |
| HTTPS | Required | Optional |
| TLS version | 1.2 minimum | 1.0/1.1 |
| Password minimum length | 12 characters | Less than 8 |
| Session timeout | 30 minutes | No timeout |
| Account lockout | After 5 failures | No lockout |
| Debug mode | Disabled | Enabled |
| Directory listing | Disabled | Enabled |
| Server version headers | Removed | Included |
| Detailed error messages | Disabled | Enabled |
| Rate limiting | Enabled | Disabled |