Skip to content

Services, Wiki-Artikel, Blog-Beiträge und Glossar-Einträge durchsuchen

↑↓NavigierenEnterÖffnenESCSchließen

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. [1] OWASP Secure Coding Practices Quick Reference Guide - OWASP
  2. [2] OWASP Secure Development Lifecycle - OWASP
  3. [3] Microsoft Security Development Lifecycle - Microsoft
  4. [4] NIST Secure Software Development Framework (SSDF) - NIST
  5. [5] OWASP Application Security Verification Standard (ASVS) - OWASP

Questions about this topic?

Our experts advise you free of charge and without obligation.

Free Consultation

About the Author

Chris Wojzechowski
Chris Wojzechowski

Geschäftsführender Gesellschafter

E-Mail

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)
IT-Grundschutz-Praktiker (TÜV) IT Risk Manager (DGI) § 8a BSIG Prüfverfahrenskompetenz Ausbilderprüfung (IHK)
This article was last edited on 08.03.2026. Responsible: Chris Wojzechowski, Geschäftsführender Gesellschafter at AWARE7 GmbH. License: CC BY 4.0 - free use with attribution: "AWARE7 GmbH, https://a7.de"

Cookielose Analyse via Matomo (selbst gehostet, kein Tracking-Cookie). Datenschutzerklärung