← Alle Artikel
Zuletzt aktualisiert: 2026-03-30

Security-fokussiertes Code Review: Checkliste für Python und TypeScript

Security Code Review Checkliste. SQL Injection, XSS, Secrets, Input Validation — für Python und TypeScript.

Zusammenfassung

Ein Code Review, das nur prüft "funktioniert es?", übersieht die teuersten Bugs: Sicherheitslücken. Dieser Leitfaden gibt Ihnen eine systematische Checkliste für security-fokussierte Code Reviews in Python- und TypeScript-Projekten. Er behandelt Injection-Angriffe, Authentifizierungsfehler, Secrets-Exposition und sprachspezifische Fallen — plus die automatisierten Tools, die laufen sollten, bevor überhaupt ein Mensch etwas reviewt.

Voraussetzungen

Schritt 1: Universelle Sicherheitscheckliste

Diese gelten für jede Codebasis, unabhängig von der Sprache.

Eingabevalidierung

[ ] Alle Benutzereingaben werden serverseitig validiert (clientseitig ist UX, nicht Sicherheit)
[ ] Eingabelängenlimits werden durchgesetzt
[ ] Datei-Uploads: Typ, Größe und Name werden serverseitig validiert
[ ] API-Request-Bodies werden gegen ein Schema validiert (Pydantic, Zod, etc.)
[ ] URL-Parameter, Header und Cookies werden als nicht vertrauenswürdig behandelt
[ ] Keine rohen Benutzereingaben werden übergeben an:
    - SQL-Abfragen (parametrisierte Queries verwenden)
    - Shell-Befehle (subprocess mit Liste, nicht shell=True)
    - Dateipfade (Allowlists, keine nutzergesteuerten Pfade)
    - Reguläre Ausdrücke (ReDoS-Risiko)
    - Template Engines (SSTI-Risiko)
    - eval() / exec() / Function()-Konstruktoren

Ausgabekodierung

[ ] HTML-Ausgabe wird automatisch von der Template Engine escaped
[ ] JSON-Antworten setzen Content-Type: application/json
[ ] Nutzerdaten in URLs sind korrekt kodiert
[ ] CSV/Excel-Exporte escapen Formel-Injection (=, +, -, @, \t, \r)
[ ] Fehlermeldungen leaken keine Stacktraces, Dateipfade oder DB-Details

Datenexposition

[ ] API-Antworten enthalten nicht mehr Daten als der Client braucht
[ ] Passwortfelder sind nie in API-Antworten enthalten
[ ] Logging enthält keine Passwörter, Tokens, PII oder Kreditkartennummern
[ ] Debug-Modus ist in der Produktionskonfiguration deaktiviert
[ ] Source Maps werden nicht in Produktion deployed (oder sind geschützt)

Schritt 2: Python-spezifisches Security Review

SQL Injection

# VERWUNDBAR — String-Formatierung in SQL
cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
cursor.execute("SELECT * FROM users WHERE email = '%s'" % email)
cursor.execute("SELECT * FROM users WHERE email = '" + email + "'")

# SICHER — Parametrisierte Abfrage
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))

# SICHER — ORM (SQLAlchemy)
User.query.filter_by(email=email).first()

# ACHTUNG — raw() im Django ORM
# Auch mit ORM kann rohes SQL injiziert werden
User.objects.raw(f"SELECT * FROM users WHERE email = '{email}'")  # VERWUNDBAR
User.objects.raw("SELECT * FROM users WHERE email = %s", [email])  # SICHER

Gefährliche Funktionen

# NIEMALS mit nicht vertrauenswürdigen Eingaben verwenden:
eval(user_input)           # Beliebige Code-Ausführung
exec(user_input)           # Beliebige Code-Ausführung
os.system(user_input)      # Shell Injection
os.popen(user_input)       # Shell Injection
subprocess.call(cmd, shell=True)  # Shell Injection wenn cmd Nutzereingaben enthält
__import__(user_input)     # Beliebiges Modulladen
compile(user_input, ...)   # Code-Kompilierung

# SICHERE Alternative für subprocess:
subprocess.run(["ls", "-la", directory], shell=False, check=True)

# NIEMALS nicht vertrauenswürdige Daten deserialisieren mit:
import pickle
pickle.loads(untrusted_data)  # Beliebige Code-Ausführung

import yaml
yaml.load(untrusted_data)     # Beliebige Code-Ausführung (yaml.safe_load verwenden)

import marshal
marshal.loads(untrusted_data) # Beliebige Code-Ausführung

Path Traversal

# VERWUNDBAR — Nutzer kontrolliert Dateipfad
def download(filename):
    return send_file(f"/uploads/{filename}")  # ../../../etc/passwd

# SICHER — Pfad validieren und normalisieren
import os
def download(filename):
    # Verzeichniswechsel entfernen
    safe_name = os.path.basename(filename)
    full_path = os.path.join("/uploads", safe_name)
    # Prüfen ob aufgelöster Pfad im erlaubten Verzeichnis liegt
    if not os.path.realpath(full_path).startswith("/uploads"):
        abort(403)
    return send_file(full_path)

SSRF (Server-Side Request Forgery)

# VERWUNDBAR — Nutzer kontrolliert URL
import requests
def fetch_url(url):
    return requests.get(url).text  # Kann interne Services, Cloud-Metadaten erreichen

# SICHER — URL-Schema und Host validieren
from urllib.parse import urlparse

ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}

def fetch_url(url):
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise ValueError("Ungültiges Schema")
    if parsed.hostname not in ALLOWED_HOSTS:
        raise ValueError("Host nicht erlaubt")
    # Auch private IP-Bereiche blocken: 10.x, 172.16-31.x, 192.168.x, 169.254.x
    return requests.get(url, timeout=5).text

Schritt 3: TypeScript-spezifisches Security Review

XSS (Cross-Site Scripting)

// VERWUNDBAR — innerHTML mit Nutzerdaten
document.getElementById("output").innerHTML = userInput;

// VERWUNDBAR — React dangerouslySetInnerHTML
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// SICHER — textContent (kein HTML-Parsing)
document.getElementById("output").textContent = userInput;

// SICHER — React escaped standardmäßig
<div>{userInput}</div>  // Das ist sicher

// ACHTUNG — URLs in href-Attributen
// VERWUNDBAR: javascript:-Protokoll
<a href={userProvidedUrl}>Klick</a>  // javascript:alert(1)

// SICHER — URL-Protokoll validieren
const isValidUrl = (url: string): boolean => {
  try {
    const parsed = new URL(url);
    return ["http:", "https:"].includes(parsed.protocol);
  } catch {
    return false;
  }
};

Prototype Pollution

// VERWUNDBAR — Deep Merge ohne Schutz
function deepMerge(target: any, source: any) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = deepMerge(target[key] || {}, source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}
// Angriff: deepMerge({}, JSON.parse('{"__proto__": {"isAdmin": true}}'))
// Jetzt hat JEDES Objekt isAdmin === true

// SICHER — Gefährliche Keys prüfen
const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);

function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>) {
  for (const key of Object.keys(source)) {
    if (FORBIDDEN_KEYS.has(key)) continue;
    if (typeof source[key] === "object" && source[key] !== null && !Array.isArray(source[key])) {
      target[key] = safeMerge(
        (target[key] as Record<string, unknown>) || {},
        source[key] as Record<string, unknown>
      );
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// BESSER — Object.create(null) für Lookup-Objekte
const config = Object.create(null);  // Keine Prototype-Chain

SQL Injection in TypeScript ORMs

// VERWUNDBAR — Prisma Raw Queries
await prisma.$queryRaw(`SELECT * FROM users WHERE email = '${email}'`);

// SICHER — Prisma Tagged Template (auto-parametrisiert)
await prisma.$queryRaw`SELECT * FROM users WHERE email = ${email}`;

// VERWUNDBAR — TypeORM
await repo.query(`SELECT * FROM users WHERE email = '${email}'`);

// SICHER — TypeORM Parameter
await repo.query("SELECT * FROM users WHERE email = $1", [email]);

// SICHER — TypeORM Query Builder
await repo.createQueryBuilder("user")
  .where("user.email = :email", { email })
  .getOne();

Unsichere Abhängigkeiten

// Auf bekannte Schwachstellen prüfen
npm audit

// Auf veraltete Pakete prüfen
npm outdated

// WICHTIG: postinstall-Skripte in package.json prüfen
// Schadpakete können Code während npm install ausführen
// Prüfen: Braucht dieses Paket ein postinstall-Skript?
// Tool: 'npm install --ignore-scripts' für nicht vertrauenswürdige Pakete

Schritt 4: Authentifizierung und Autorisierung prüfen

Authentifizierungs-Checkliste

[ ] Passwörter sind mit bcrypt, scrypt oder argon2 gehasht (nie MD5/SHA)
[ ] Passwort-Reset-Tokens sind einmalig und zeitbegrenzt (max 1 Stunde)
[ ] Session-Tokens sind kryptographisch zufällig (min 128 Bit)
[ ] Cookies haben: HttpOnly, Secure, SameSite=Strict/Lax Flags
[ ] JWT-Tokens: kurze Gültigkeit, Signatur verifiziert, Algorithmus gepinnt (kein "none")
[ ] Rate Limiting auf Login-Endpoints (Brute Force verhindern)
[ ] Kontosperrung nach N Fehlversuchen (mit Benachrichtigung)
[ ] MFA ist für sensible Operationen verfügbar

Autorisierungs-Checkliste

[ ] Jeder API-Endpoint prüft Autorisierung (nicht nur Authentifizierung)
[ ] IDOR-Schutz: Nutzer können nur auf eigene Ressourcen zugreifen
    (user.id === resource.ownerId, serverseitig geprüft)
[ ] Rollenprüfungen sind zentralisiert, nicht über Handler verstreut
[ ] Admin-Endpoints in separater Route-Gruppe mit Middleware
[ ] Dateizugriff prüft Eigentümerschaft, nicht nur Authentifizierung
[ ] Rollen-/Berechtigungsänderung erfordert erneute Authentifizierung

Häufiges IDOR-Muster

# VERWUNDBAR — keine Eigentümerprüfung
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int):
    return db.query(Invoice).get(invoice_id)  # Jeder Nutzer sieht jede Rechnung

# SICHER — Eigentümerprüfung
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int, current_user: User = Depends(get_current_user)):
    invoice = db.query(Invoice).filter(
        Invoice.id == invoice_id,
        Invoice.user_id == current_user.id  # Eigentümerprüfung
    ).first()
    if not invoice:
        raise HTTPException(404)
    return invoice

Schritt 5: Secrets und Konfiguration prüfen

Secrets-Scan

# Nach hardcodierten Secrets in der Codebasis suchen
# Diese ausführen und jeden Treffer prüfen:

# API Keys
grep -rn "api_key\|apikey\|api-key" --include="*.py" --include="*.ts" .

# Passwörter
grep -rn "password\s*=\|passwd\|secret" --include="*.py" --include="*.ts" .

# AWS Keys
grep -rn "AKIA[0-9A-Z]\{16\}" .

# Private Keys
grep -rn "BEGIN.*PRIVATE KEY" .

# Connection Strings
grep -rn "postgresql://\|mysql://\|mongodb://\|redis://" .

Konfigurations-Review

[ ] .env-Dateien sind in .gitignore
[ ] Keine .env-Dateien in der Repository-History (prüfen: git log --all --full-history -- .env)
[ ] Verschiedene Konfigurationen für Dev/Staging/Produktion
[ ] Produktionskonfigurationen haben:
    - DEBUG=false
    - Strikte CORS-Origins (kein Wildcard *)
    - HTTPS-only Cookies
    - Rate Limiting aktiviert
    - Logging konfiguriert (ohne PII)
[ ] Umgebungsvariablen für alle Secrets (nie in Code/Config-Dateien)
[ ] Standard-Passwörter wurden geändert

Schritt 6: Automatisierte Security-Tools

Python-Toolchain

# Bandit — findet häufige Sicherheitsprobleme in Python
pip install bandit
bandit -r src/ -f json -o bandit-report.json

# Safety — prüft auf bekannt verwundbare Abhängigkeiten
pip install safety
safety check --json

# Semgrep — musterbasierte Code-Analyse
pip install semgrep
semgrep --config=p/python --config=p/security-audit src/

# Pre-commit Hook (.pre-commit-config.yaml)
repos:
  - repo: https://github.com/PyCQA/bandit
    rev: '1.8.3'
    hooks:
      - id: bandit
        args: ['-c', 'bandit.yaml']

TypeScript-Toolchain

# ESLint mit Security-Plugins
npm install --save-dev eslint-plugin-security eslint-plugin-no-unsanitized

// .eslintrc.json
{
  "plugins": ["security", "no-unsanitized"],
  "extends": ["plugin:security/recommended"],
  "rules": {
    "security/detect-object-injection": "warn",
    "security/detect-non-literal-require": "error",
    "security/detect-eval-with-expression": "error",
    "no-unsanitized/method": "error",
    "no-unsanitized/property": "error"
  }
}

# npm audit — prüft Abhängigkeiten
npm audit --production

# Snyk — umfassendere Abhängigkeitsanalyse
npx snyk test
npx snyk code test  # SAST (statische Analyse)

CI/CD-Integration

# GitHub Actions Beispiel
name: Security Scan
on: [pull_request]
jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Bandit ausführen (Python)
        run: pip install bandit && bandit -r src/ --severity-level medium
      - name: npm audit ausführen (TypeScript)
        run: npm audit --audit-level=high
      - name: Snyk ausführen
        uses: snyk/actions@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      - name: Semgrep ausführen
        uses: returntocorp/semgrep-action@v1
        with:
          config: p/security-audit

Problembehandlung & Hinweise

"Das Team sieht Security Reviews als Blocker"

Automatisieren Sie die offensichtlichen Prüfungen. Bandit, ESLint Security Rules und npm audit sollten in CI laufen, bevor ein Mensch den PR anschaut. Das menschliche Review fokussiert dann auf Logikfehler, Autorisierung und Business-Logic-Sicherheit — Dinge, die Tools nicht erkennen. Das reduziert die Review-Reibung erheblich.

"Zu viele False Positives von automatisierten Tools"

Stimmen Sie die Tools ab. Bandit unterstützt pro-Datei- und pro-Zeilen-Unterdrückung (# nosec) mit Begründung. ESLint-Regeln können pro Verzeichnis konfiguriert werden. Erstellen Sie eine Baseline bekannter Issues und fokussieren Sie auf neue Befunde. Aber: Deaktivieren Sie nie global eine Security-Regel, weil sie "nervig" ist.

"Wir nutzen ein Framework — sind wir nicht sicher?"

Frameworks schützen Sie vor häufigen Fehlern, wenn sie korrekt verwendet werden. Django auto-escaped Templates (aber der |safe-Filter umgeht das). React auto-escaped JSX (aber dangerouslySetInnerHTML umgeht das). ORMs verhindern SQL Injection (aber raw()-Queries umgehen das). Das Framework ist ein Sicherheitsnetz mit Löchern. Prüfen Sie die Löcher.

"Wir haben keine Zeit für Security Reviews"

Für einen Datenschutzvorfall haben Sie auch keine Zeit. Ein security-fokussiertes Review fügt 15-30 Minuten pro PR hinzu. Ein Datenleck kostet Monate und potenziell das Geschäft. Starten Sie mit der automatisierten Toolchain — die braucht 30 Minuten zum Einrichten und läuft dann automatisch für immer.

Prävention & Best Practices

Shift Left

Security-Tools sollten auf jedem Entwicklerrechner laufen (Pre-commit Hooks) und in CI. Warten Sie nicht auf ein Security Review am Ende. Zu dem Zeitpunkt ist der Code "fertig" und niemand will ihn umschreiben.

Security Champions

Ernennen Sie einen Entwickler pro Team zum "Security Champion". Er nimmt an Security-Trainings teil, bleibt bei Schwachstellen aktuell und reviewt die Sicherheitsaspekte von PRs. Rotieren Sie die Rolle vierteljährlich, um Wissen zu verbreiten.

Abhängigkeits-Hygiene

Führen Sie npm audit und safety check wöchentlich aus, nicht nur in CI. Abonnieren Sie Sicherheitshinweise für Ihre wichtigsten Abhängigkeiten. Nutzen Sie Dependabot oder Renovate für automatisierte Dependency-Updates. Pinnen Sie exakte Versionen in Produktion (package-lock.json, requirements.txt mit Hashes).

Bedrohungsmodellierung

Bevor Sie ein Security Review machen, wissen Sie, was Sie schützen. Verbringen Sie 30 Minuten mit dem Team und fragen: Welche Daten sind sensibel? Was sind die Angriffsflächen? Was passiert bei Kompromittierung? Die Antworten bestimmen, worauf Sie den Review-Fokus legen.

Review-Checkliste als Code

Integrieren Sie die Security-Checkliste in das PR-Template. Jeder PR sollte einen Abschnitt haben, in dem der Autor bestätigt, dass er die relevanten Sicherheitspunkte geprüft hat. Das normalisiert Sicherheitsdenken als Teil des Entwicklungsprozesses, nicht als Nachgedanke.

Experten-Hilfe gebraucht?

Professionelles Code Review? €150, 2-Stunden Deep Dive + schriftlicher Bericht.

Jetzt buchen — €150

100% Geld-zurück-Garantie

HR

Harald Roessler

Infrastructure Engineer mit 20+ Jahren Erfahrung. Gründer der DSNCON GmbH.