Secure Coding Practices: Anchoring security in the development process
Secure Coding and Secure SDLC: From threat modeling and STRIDE to language-specific security patterns (Python, Java, Node.js, Go) for input validation, SQL injection, authentication, and cryptography, through to SAST/DAST/SCA in CI/CD, security code reviews, container security, SBOM, secrets management, and the OWASP SAMM maturity model. A practical guide for development teams without their own security department.
Table of Contents (14 sections)
Security vulnerabilities usually arise not from a lack of intent, but from a lack of habits. Secure coding means internalizing the right patterns until they become second nature. Adding security to the SDLC after the fact is like putting on a seatbelt after an accident: expensive, ineffective, and too late. A bug in the design costs around 100 EUR, in the code 1,000 EUR, and in production 100,000 EUR or more.
The Secure SDLC (Software Development Lifecycle)
Traditional SDLC:
Requirements → Design → Coding → Testing → Deployment → Maintenance
↑
Security testing only at this stage!
Secure SDLC (Microsoft SDL / OWASP SDLC):
Requirements → Define security requirements
Design → Threat Modeling (STRIDE/PASTA)
Coding → Secure Coding Guidelines, SAST in the IDE
Testing → DAST, Penetration Testing, SCA
Deployment → Security Review, Secure Config
Maintenance → Patch Management, Vulnerability Monitoring
Basic Principles of Secure Coding
The ten most important secure coding principles:
1. Input Validation (All inputs are suspicious):
→ Validate EVERYTHING: HTTP parameters, headers, cookies, file contents
→ Whitelist-based: what is ALLOWED, not what is forbidden
→ Validation: as early as possible, as close to the input as possible
2. Output Encoding (Context-dependent):
→ HTML Context: HTML Entity Encoding (< → <)
→ JavaScript Context: JavaScript Encoding (\x3C)
→ SQL Context: Parameterized Queries (never string concatenation!)
→ URL Context: URL Encoding (%3C)
→ Incorrect Context = XSS, Injection!
3. Authentication (Verify Identity):
→ Strong password hashing algorithms (bcrypt, Argon2, PBKDF2)
→ MFA for privileged actions
→ Session Management: random session IDs, short lifetime
4. Authorization (Control access):
→ Object-level authorization for every resource
→ Least Privilege: minimal permissions
→ Deny-by-default: what is not explicitly allowed is prohibited
5. Fail Secure (On error: deny access):
→ Exception = Deny access, do not grant it
→ No default credentials
→ Secure defaults everywhere
6. Separation of Concerns:
→ Strictly separate data and code (SQL, HTML, Shell)
→ Template engines: User input as variables, never in templates
7. Defense in Depth:
→ Multiple independent security layers
→ A single vulnerability does not compromise everything
8. Cryptography: Only use verified libraries:
→ NEVER implement your own cryptography!
→ Secure defaults: AES-256-GCM, RSA-4096 or Ed25519, bcrypt
9. Error Handling: No sensitive details:
→ Log internal errors, but do not pass them to the frontend
→ Generic error messages for attackers, correlation ID for support
10. Logging and Monitoring:
→ Log security-relevant events (logins, access attempts)
→ No sensitive data in logs (passwords, tokens, PII)
Phase 1: Security Requirements and Design
OWASP Application Security Verification Standard (ASVS):
Level 1: Basic Security (all applications)
Level 2: Standard Security (business-critical)
Level 3: Highest Security (critical infrastructure)
Key ASVS Requirements (Level 1):
V1: Architecture, Design, and Threat Model
V2: Authentication (MFA, Session Management)
V3: Session Management (secure tokens, timeout)
V5: Input Validation and Encoding (prevent XSS, SQLi)
V6: Cryptography (AES-256, TLS 1.2+, no MD5!)
V8: Data protection (logging, error handling without data leaks)
V9: Communication security (TLS, certificate pinning)
V14: Configuration security (secrets, dependencies)
Threat Modeling with STRIDE
STRIDE Threat Model - Analyze per Component:
Data Flow Diagram (DFD):
External Users
│
▼
[Browser] ──HTTPS──► [Web App Server] ──► [DB Server]
│
└──► [Auth Service]
Trust Boundaries:
Internet → DMZ → Intranet → Database
STRIDE per component (example: Web App Server):
S (Spoofing - Identity Forgery):
→ Question: Can an attacker impersonate someone else?
→ Countermeasures: MFA, strong authentication, JWT signing
T (Tampering):
→ Question: Can an attacker alter data?
→ Countermeasures: Signatures, MAC, input validation, HMAC
R (Repudiation):
→ Question: Can someone deny having performed an action?
→ Countermeasures: immutable audit logs, digital signatures
I (Information Disclosure):
→ Question: Is sensitive data inadvertently disclosed?
→ Countermeasures: error handling, encryption, minimal disclosure
D (Denial of Service):
→ Question: Can the service be brought down?
→ Countermeasures: Rate limiting, input size limits, circuit breakers
E (Elevation of Privilege):
→ Question: Can a user gain more privileges than allowed?
→ Countermeasures: Least privilege, RBAC, IDOR protection
Security Story Template:
"As an [attacker], I want to [attack action] in order to achieve [goal]."
Example: "As an attacker, I want to use SQL injection to read the customer database
in order to steal credit card data."
→ For each security story: define a corresponding test case
Input Validation Patterns
Correct Input Validation (multiple languages):
Python (general):
import re, html
from email_validator import validate_email, EmailNotValidError
def validate_username(username: str) -> str:
if not isinstance(username, str):
raise ValueError("Username must be a string")
if not 3 <= len(username) <= 50:
raise ValueError("Username: 3-50 characters")
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
raise ValueError("Only letters, numbers, _, and -")
return username
safe_output = html.escape(user_input) # Safely escape HTML output
try:
valid = validate_email(email, check_deliverability=False)
safe_email = valid.email
except EmailNotValidError:
raise ValueError("Invalid email")
Java (Spring Bean Validation - JSR-380):
public class UserRequest {
@NotBlank @Size(min=3, max=50)
@Pattern(regexp = "^[a-zA-Z0-9_-]+$",
message = "Only allowed characters")
private String username;
@Email @NotBlank
private String email;
@Min(0) @Max(150)
private Integer age;
}
Node.js (Zod):
import { z } from 'zod';
const UserSchema = z.object({
username: z.string()
.min(3).max(50)
.regex(/^[a-zA-Z0-9_-]+$/),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional()
});
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json(result.error.flatten());
}
File upload validation:
ALLOWED_MIME_TYPES = {'image/jpeg', 'image/png', 'image/webp'}
MAX_SIZE_BYTES = 5 * 1024 * 1024 # 5MB
import magic # python-magic reads MagicBytes, not just the extension!
mime = magic.from_buffer(file.read(1024), mime=True)
if mime not in ALLOWED_MIME_TYPES:
raise ValueError(f"Invalid file type: {mime}")
# Check MagicBytes, NOT the filename!
# upload.jpg.php → MagicBytes = PHP → blocked!
Prevent path traversal:
import os
def safe_join(base, user_path):
path = os.path.realpath(os.path.join(base, user_path))
if not path.startswith(base):
raise ValueError("Path traversal attempt detected!")
return path
SQL Injection Prevention
Secure database queries - the basic principle:
Python (sqlite3/psycopg2):
# WRONG - SQL injection possible:
query = f"SELECT * FROM users WHERE username='{username}'"
cursor.execute(query)
# CORRECT - Parameterized query:
cursor.execute(
"SELECT * FROM users WHERE username = %s",
(username,)
)
# SQLAlchemy ORM (even more secure):
user = db.session.query(User).filter_by(username=username).first()
Java (PreparedStatement):
// WRONG:
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT * FROM users WHERE id=" + userId);
// CORRECT:
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM users WHERE id = ?");
ps.setInt(1, userId);
ResultSet rs = ps.executeQuery();
Node.js (pg):
// WRONG:
const result = await pool.query(
`SELECT * FROM orders WHERE user_id='${userId}'`);
// CORRECT:
const result = await pool.query(
'SELECT * FROM orders WHERE user_id = $1',
[userId]
);
// Prisma ORM (secure by design):
const user = await prisma.user.findUnique({
where: { id: userId }
});
NoSQL Injection (MongoDB):
// WRONG:
db.users.find({ username: req.body.username })
// If username = { $gt: "" } → all users!
// CORRECT - Type Check:
if (typeof req.body.username !== 'string') {
return res.status(400).send('Invalid input');
}
// NEVER: db.users.find({ $where: "this.field..." })
Authentication and Session Management
Secure Password Management:
Algorithms:
CORRECT: bcrypt (widely used, robust), Argon2id (latest standard)
scrypt (memory-hard), PBKDF2 (FIPS-compliant)
WRONG: MD5, SHA-1, SHA-256 alone!
Without salt: Rainbow table attacks possible!
Too fast: Brute force in seconds!
Python (Argon2id):
from argon2 import PasswordHasher
ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=2)
hash = ph.hash("user-password")
try:
ph.verify(hash, "user-password")
if ph.check_needs_rehash(hash):
hash = ph.hash("user-password") # Re-hash with new parameters
except VerifyMismatchError:
raise ValueError("Incorrect password")
Node.js (bcrypt):
const bcrypt = require('bcrypt');
const ROUNDS = 12;
const hash = await bcrypt.hash(password, ROUNDS);
const valid = await bcrypt.compare(password, hash);
Java (BCryptPasswordEncoder):
PasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hashed = encoder.encode(rawPassword);
boolean valid = encoder.matches(rawPassword, hashed);
Session Management:
# Session ID: cryptographically random, min. 128 bits:
import secrets
session_id = secrets.token_hex(32) # 256 bits
# Session cookie attributes:
response.set_cookie(
'session', session_id,
httponly=True, # No JS access
secure=True, # HTTPS only
samesite='Strict', # CSRF protection
max_age=3600 # 1-hour lifetime
)
# Session rotation after login (session fixation protection!):
old_session_data = get_session(old_id)
new_id = secrets.token_hex(32)
create_session(new_id, old_session_data)
delete_session(old_id)
JWT (use correctly):
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET, # strong, from env!
{ expiresIn: '1h', algorithm: 'RS256' } # asymmetric!
);
# NEVER: algorithms=["none"] or without specifying an algorithm!
Secrets Management
WRONG: Hardcoded Secrets
DATABASE_URL = "postgresql://admin:SuperSecret123@db.firma.de/prod" # in Git!
RIGHT: Environment Variables
DB_PASSWORD = os.environ.get('DB_PASSWORD')
BETTER: Secret Management System
import hvac # HashiCorp Vault
client = hvac.Client()
secret = client.secrets.kv.read_secret_version(path='my-app/db')
DB_PASSWORD = secret['data']['data']['password']
# Azure Key Vault:
from azure.keyvault.secrets import SecretClient
# AWS Secrets Manager, GCP Secret Manager (similar)
.gitignore: always add .env!
echo ".env" >> .gitignore
Pre-commit hooks for secret detection:
pip install detect-secrets
detect-secrets scan > .secrets.baseline
# Gitleaks: pre-commit hook in .pre-commit-config.yaml
Cryptography Patterns in Development
Secure Cryptography in Applications:
Symmetric Encryption (AES-GCM):
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
nonce = os.urandom(12) # 96-bit nonce, ONE-TIME!
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
stored = nonce + ciphertext # Nonce stored
nonce = stored[:12]
ciphertext = stored[12:]
plaintext = aesgcm.decrypt(nonce, ciphertext, associated_data)
# IMPORTANT: Never reuse a nonce! (GCM nonce misuse attack)
Asymmetric signatures (Ed25519):
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
signature = private_key.sign(message)
public_key.verify(signature, message) # Raises an exception if invalid
Random numbers for security purposes:
Python: secrets.token_bytes(32) # Cryptographically secure!
Java: SecureRandom.getInstanceStrong()
Node.js: crypto.randomBytes(32)
Go: crypto/rand.Read()
NEVER use for security:
Python: random.random() → NOT cryptographically secure!
Java: Math.random() → NOT cryptographically secure!
JS: Math.random() → NOT cryptographically secure!
Error handling and secure logging
No internal details to the frontend:
WRONG:
try:
result = database_query(user_id)
except Exception as e:
return {"error": str(e)} # DB structure, table names, SQL details!
RIGHT:
import uuid
try:
result = database_query(user_id)
except DatabaseError as e:
error_id = str(uuid.uuid4())
logger.error(f"[{error_id}] DB error for user {user_id}: {e}", exc_info=True)
return {"error": "Internal error", "reference": error_id}, 500
Secure logging:
NEVER log:
□ Passwords (including failed logins!)
□ Session IDs / tokens
□ Credit card numbers (PCI-DSS!)
□ Private keys
□ Full PII (GDPR!)
Masking sensitive data:
def mask_sensitive(data: dict) -> dict:
SENSITIVE_KEYS = {'password', 'token', 'credit_card', 'secret'}
return {
k: '***' if k.lower() in SENSITIVE_KEYS else v
for k, v in data.items()
}
Structured Logging (for SIEM integration):
import structlog
log = structlog.get_logger()
log.warning(
"failed_login",
username=username,
ip=request.remote_addr,
user_agent=request.headers.get('User-Agent'),
timestamp=datetime.utcnow().isoformat()
)
# JSON format → easily importable into SIEM
Security Testing in CI/CD
Automated security checks:
SAST (Static Application Security Testing):
Semgrep (Open Source):
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/python
p/javascript
p/docker
CodeQL (GitHub Actions, free for open source):
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: python, javascript
What SAST finds:
→ SQL injection patterns
→ Hardcoded secrets (password in code)
→ Insecure random numbers (Math.random instead of crypto.randomBytes)
→ Missing input validation
→ Insecure configurations (CORS: *)
SCA (Software Composition Analysis):
Trivy (All-in-one scanner):
trivy image myapp:latest # Scan container
trivy fs --scanners vuln,secret,misconfig ./ # Filesystem
trivy image --format cyclonedx --output sbom.json myapp:latest # SBOM
Python SCA:
pip install pip-audit
pip-audit --vulnerability-service pypi
Node.js:
npm audit --audit-level=high
Dependabot (.github/dependabot.yml):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
Secrets Scanning:
# Gitleaks (CI):
- name: Gitleaks Secret Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Trufflehog (deeper scan including commit history):
- name: TruffleHog OSS
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
DAST (Dynamic Application Security Testing):
OWASP ZAP Baseline Scan in CI/CD:
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.10.0
with:
target: 'https://staging.meineapp.de'
cmd_options: '-T 120'
Nuclei (Template-based):
nuclei -u https://staging.meinapp.de -t cves/ -t misconfigurations/
Container Security
Dockerfile Best Practices:
WRONG:
FROM ubuntu:latest
RUN apt-get install -y python3 && pip install -r requirements.txt
USER root # never!
CORRECT:
FROM python:3.12-slim # minimal image
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Non-root user:
RUN useradd --create-home appuser
USER appuser
# Health check:
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["python", "app.py"]
Dependency Pinning:
→ Always commit package-lock.json / yarn.lock / poetry.lock! (reproducible builds)
→ Update regularly (Dependabot/Renovate)
→ pip: requirements.txt with exact versions:
flask==3.0.1 # not flask>=3.0.0!
→ Docker: never use "latest" in production:
FROM python:3.12.1-slim # specific version!
Security Release Gates
Mandatory checks before deployment:
Definition of Done (Security):
→ SAST: 0 critical findings (Semgrep/SonarQube)
→ SCA: 0 critical CVEs in dependencies (Trivy/Snyk)
→ Secrets Scan: 0 secrets in code (GitLeaks/detect-secrets)
→ DAST Baseline: 0 High/Critical in staging (ZAP)
→ Security Code Review: approved
GitHub Actions Security Gate:
security-gate:
runs-on: ubuntu-latest
needs: [sast, sca, secrets-scan]
steps:
- name: Check security scan results
run: |
if [ -f sast-findings.json ]; then
CRITICAL=$(jq '[.[] | select(.severity == "CRITICAL")] | length' sast-findings.json)
if [ "$CRITICAL" -gt 0 ]; then
echo "FAIL: $CRITICAL critical SAST findings!"
exit 1
fi
fi
SBOM (Software Bill of Materials):
→ ISO/IEC 5962, SPDX, or CycloneDX format
→ Mandatory for: U.S. government contracts (Executive Order 2021)
→ Recommended: KRITIS operators, critical applications
→ Syft: syft myapp:latest -o cyclonedx-json > sbom.json
Security Code Review Checklist
Quick Reference for Code Reviews:
Injection Testing:
□ SQL: Parameterized queries everywhere? (grep for string concatenation with SQL)
□ Command Injection: Shell calls → only execFile with array arguments
□ SSTI: Template strings with user input? (only variables, never inline!)
□ XXE: XML parser configured? (external entities disabled)
□ LDAP Injection: LDAP queries escaped?
□ NoSQL Injection: Type check before MongoDB queries?
Authentication & Session:
□ Password hashing: bcrypt/Argon2? (no MD5, SHA-1, SHA-256 alone!)
□ Session IDs: cryptographically random (crypto.randomBytes)?
□ Session rotation after login?
□ CSRF protection on state-changing endpoints?
□ Session timeout implemented?
□ Logout: Session invalidated on the server side?
Authorization:
□ All endpoints: Authentication enforced?
□ Every resource: Ownership check (not just auth check! IDOR!)
□ RBAC/ABAC correctly implemented?
□ Admin functions: double check?
□ Hidden fields/parameters: validated on the server side (do not trust!)?
Cryptography:
□ No self-implemented cryptography?
□ Strong algorithms: AES-256-GCM, RSA-4096/Ed25519, bcrypt/Argon2?
□ Nonce reuse avoided?
□ Secrets not in the code (env-vars or Secrets Manager)?
□ TLS: Version 1.2+ enforced?
Input & Output:
□ All user inputs: validated and sanitized?
□ SQL: exclusively parameterized queries?
□ HTML output: escaped (XSS protection)?
□ File uploads: type/size/name validated?
□ Error Messages: no stack traces, no internal info!
Logging:
□ No sensitive data in logs (passwords, tokens, credit cards!)?
□ Security events logged (login, errors, unauthorized access)?
□ Logs cannot be manipulated by user input (log injection)?
Dependencies:
□ npm audit / pip audit / mvn dependency-check executed?
□ Known CVEs in dependencies?
□ No outdated cryptography libraries?
□ New dependencies: Security review performed?
Maturity Model: OWASP SAMM
OWASP SAMM (Software Assurance Maturity Model) - Maturity Levels:
Level 1 (Basic):
✓ OWASP Top 10 training for developers
✓ Manual security review before release
✓ Basic SAST in IDE
✓ Security user stories
Level 2 (Advanced):
✓ Threat modeling for new features
✓ SAST + SCA in CI/CD pipeline
✓ DAST on staging environment
✓ Security champions in development teams
✓ Security release gates
Level 3 (Leading):
✓ Continuous penetration testing (bug bounty)
✓ Automated DAST in Production
✓ Security Architecture Reviews
✓ Red Team Exercises
✓ SBOM for all releases
For most German SMEs, Levels 1–2 are realistic and protect
against 80% of the most common vulnerabilities. An annual penetration test
reveals what CI/CD tools overlook. Sources & References
- [1] OWASP Secure Coding Practices Quick Reference Guide - OWASP
- [2] OWASP Secure Development Lifecycle - OWASP
- [3] Microsoft Security Development Lifecycle - Microsoft
- [4] NIST Secure Software Development Framework (SSDF) - NIST
- [5] OWASP Application Security Verification Standard (ASVS) - OWASP
Questions about this topic?
Our experts advise you free of charge and without obligation.
About the Author
Geschäftsführender Gesellschafter der AWARE7 GmbH mit langjähriger Expertise in Informationssicherheit, Penetrationstesting und IT-Risikomanagement. Absolvent des Masterstudiengangs Internet-Sicherheit an der Westfälischen Hochschule (if(is), Prof. Norbert Pohlmann). Bestseller-Autor im Wiley-VCH Verlag und Lehrbeauftragter der ASW-Akademie. Einschätzungen zu Cybersecurity und digitaler Souveränität erschienen u.a. in Welt am Sonntag, WDR, Deutschlandfunk und Handelsblatt.
10 Publikationen
- Einsatz von elektronischer Verschlüsselung - Hemmnisse für die Wirtschaft (2018)
- Kompass IT-Verschlüsselung - Orientierungshilfen für KMU (2018)
- IT Security Day 2025 - Live Hacking: KI in der Cybersicherheit (2025)
- Live Hacking - Credential Stuffing: Finanzrisiken jenseits Ransomware (2025)
- Keynote: Live Hacking Show - Ein Blick in die Welt der Cyberkriminalität (2025)
- Analyse von Angriffsflächen bei Shared-Hosting-Anbietern (2024)
- Gänsehaut garantiert: Die schaurigsten Funde aus dem Leben eines Pentesters (2022)
- IT Security Zertifizierungen — CISSP, T.I.S.P. & Co (Live-Webinar) (2023)
- Sicherheitsforum Online-Banking — Live Hacking (2021)
- Nipster im Netz und das Ende der Kreidezeit (2017)