Zum Hauptinhalt springen

Grundlagen des sicheren Programmierens

Die meisten Sicherheitsschwachstellen in Webanwendungen entstehen durch eine kleine Gruppe häufiger Fehler. SQL-Injection, Cross-Site-Scripting, fehlerhafte Authentifizierung — das sind gut dokumentierte Schwachstellenklassen, die dennoch immer wieder in neuem Code auftauchen, weil Entwickler nicht darin geschult werden, sie zu vermeiden.

Die gute Nachricht: Sie müssen kein Sicherheitsexperte werden, um diese Fehler zu vermeiden. Das Verstehen der zehn wichtigsten Schwachstellenklassen und das konsequente Anwenden von Mustern zu deren Verhinderung beseitigt die große Mehrheit der Sicherheitsprobleme in Ihrem Code.

Dieses Kapitel behandelt die OWASP Top 10, praktische Präventationstechniken und eine Checkliste, die Ihr Team bei Code-Reviews verwenden kann.

Die OWASP Top 10

OWASP (Open Web Application Security Project) pflegt eine Liste der zehn kritischsten Sicherheitsrisiken für Webanwendungen. Die Liste wird alle paar Jahre auf Basis realer Daten aus Sicherheitsbewertungen aktualisiert.

Hier ist die aktuelle Liste (2021) mit praktischem Kontext für kleine Teams:

A01: Fehlerhafte Zugriffskontrolle

Benutzer können auf Daten oder Funktionen zugreifen, auf die sie keinen Zugriff haben sollten. Die häufigste Schwachstellenklasse.

So sieht es aus:

# BAD: Anyone can view any user's profile by changing the ID
@app.route('/api/users/<user_id>/profile')
def get_profile(user_id):
return User.query.get(user_id).to_dict()

Die Lösung:

# GOOD: Check that the requesting user can access this profile
@app.route('/api/users/<user_id>/profile')
@login_required
def get_profile(user_id):
if current_user.id != int(user_id) and not current_user.is_admin:
abort(403)
return User.query.get(user_id).to_dict()

Präventionsmuster:

  • Standardmäßig verweigern. Zugriff explizit gewähren, nicht explizit verweigern.
  • Autorisierung bei jeder Anfrage prüfen, nicht nur in der UI.
  • Indirekte Referenzen (UUIDs) statt sequentieller IDs verwenden.
  • Zugriffskontrollfehler protokollieren.

A02: Kryptografische Fehler

Sensible Daten aufgrund schwacher oder fehlender Verschlüsselung exponiert.

Häufige Fehler:

  • Passwörter im Klartext oder mit schwachen Hashfunktionen speichern (MD5, SHA1)
  • Daten über HTTP statt HTTPS übertragen
  • Verschlüsselungsschlüssel im Quellcode hardcoden
  • Veraltete kryptografische Algorithmen verwenden

Was zu tun ist:

# BAD: MD5 for passwords
password_hash = hashlib.md5(password.encode()).hexdigest()

# GOOD: bcrypt with proper cost factor
from bcrypt import hashpw, gensalt, checkpw
password_hash = hashpw(password.encode(), gensalt(rounds=12))

Präventionsmuster:

  • bcrypt, Argon2 oder scrypt für Passwort-Hashing verwenden. Niemals MD5 oder SHA1.
  • HTTPS überall erzwingen. HTTP auf HTTPS umleiten.
  • Secrets in Umgebungsvariablen oder einem Secrets Manager speichern (Passwork, HashiCorp Vault), niemals im Code.
  • TLS 1.2 oder höher für Daten im Transit verwenden.

A03: Injection

Nicht vertrauenswürdige Daten als Teil eines Befehls oder einer Abfrage an einen Interpreter gesendet. SQL-Injection ist das klassische Beispiel, umfasst aber auch NoSQL, OS-Befehle, LDAP und andere.

SQL-Injection-Beispiel:

# BAD: String concatenation with user input
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)

# GOOD: Parameterized query
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (username,))

Command-Injection-Beispiel:

# BAD: User input in shell command
os.system(f"ping {host}")

# GOOD: Use subprocess with argument list
subprocess.run(["ping", "-c", "4", host], capture_output=True)

Präventionsmuster:

  • Parametrisierte Abfragen (Prepared Statements) für jeden Datenbankzugriff verwenden.
  • ORMs verwenden, die Parametrisierung automatisch handhaben.
  • Alle Eingaben validieren und bereinigen.
  • Benutzereingaben niemals direkt an System-Befehle übergeben.

A04: Unsicheres Design

Sicherheitsprobleme, die in die Architektur eingebettet sind, nicht nur Implementierungsfehler.

Beispiele:

  • Passwortwiederherstellung, die das tatsächliche Passwort per E-Mail sendet (bedeutet, es wird abrufbar gespeichert)
  • „Sicherheitsfragen", die leicht recherchiert werden können (Mädchenname der Mutter, erstes Haustier)
  • Kein Rate Limiting auf Authentifizierungs-Endpunkten
  • Unbegrenzte Datei-Uploads ohne Größen- oder Typbeschränkungen erlauben

Präventionsmuster:

  • Threat Modeling während des Designs, nicht danach.
  • Missbrauchsfälle in Betracht ziehen: „Was wenn jemand versucht, das zu brechen?"
  • Defense in Depth anwenden — mehrere Schutzschichten.
  • Etablierte Sicherheitsdesignmuster befolgen.

A05: Sicherheitsfehlkonfiguration

Standard-Zugangsdaten, unnötige aktivierte Funktionen, ausführliche Fehlermeldungen, fehlende Sicherheits-Header.

Häufige Probleme:

  • Standard-Admin-Passwörter nicht geändert
  • Debug-Modus in der Produktion aktiviert
  • Verzeichnislisting aktiviert
  • Fehlende Sicherheits-Header (CSP, X-Frame-Options)
  • Unnötige Services laufen

Präventionsmuster:

  • Konfiguration mit Infrastructure as Code automatisieren.
  • Ungenutzte Features und Frameworks entfernen.
  • Sicherheits-Header-Prüfer verwenden (securityheaders.com).
  • Verschiedene Konfigurationen für Dev/Staging/Produktion implementieren.

A06: Anfällige und veraltete Komponenten

Verwendung von Bibliotheken oder Frameworks mit bekannten Schwachstellen.

Das Problem: Ihre Anwendung könnte sicher sein, aber wenn Sie eine Bibliothek mit einer veröffentlichten CVE verwenden, haben Angreifer einen fertigen Exploit.

Präventionsmuster:

  • Abhängigkeiten aktuell halten.
  • Dependabot, Snyk oder npm audit zur Überwachung auf Schwachstellen nutzen.
  • Ungenutzte Abhängigkeiten entfernen.
  • Sicherheitshinweise für Ihren Stack abonnieren.

Das wird im Kapitel über Schwachstellenmanagement ausführlich behandelt.

A07: Identifikations- und Authentifizierungsfehler

Schwache Passwörter, Credential Stuffing, Sitzungsverwaltungsfehler.

Häufige Probleme:

  • Kein Schutz gegen Brute Force
  • Sitzungstoken in URLs
  • Sitzungen, die nicht ablaufen
  • Passwort-Reset-Token, die nicht ablaufen

Präventionsmuster:

  • MFA implementieren (früher in diesem Kurs behandelt).
  • Etabliertes Sitzungsmanagement aus Ihrem Framework verwenden.
  • Passwort-Komplexitätsanforderungen erzwingen.
  • Authentifizierungsversuche rate-limitieren.
  • Sitzungen bei Abmeldung und Passwortänderung invalidieren.

A08: Software- und Datenintegritätsfehler

Code oder Daten wurden ohne Verifizierung geändert. Umfasst unsichere Deserialisierung und CI/CD-Pipeline-Angriffe.

Beispiele:

  • Nicht vertrauenswürdige Daten deserialisieren (Pickle, Java-Serialisierung)
  • JavaScript von nicht vertrauenswürdigen CDNs ohne Integritätsprüfungen laden
  • CI/CD-Pipelines, die beliebigen Code aus Pull Requests ausführen

Präventionsmuster:

  • Subresource Integrity (SRI) für externe Skripte verwenden.
  • Nicht vertrauenswürdige Daten nicht deserialisieren. Falls nötig, sichere Alternativen verwenden (JSON statt Pickle).
  • Code- und Datenintegrität signieren und verifizieren.
  • Ihre CI/CD-Pipeline sichern.

A09: Fehler in der Sicherheitsprotokollierung und -überwachung

Sie können Angriffe nicht erkennen oder darauf reagieren, wenn Sie keine Protokolle führen.

Was zu protokollieren ist:

  • Erfolgreiche und fehlgeschlagene Authentifizierungen
  • Autorisierungsfehler
  • Eingabevalidierungsfehler
  • Anwendungsfehler

Was nicht zu protokollieren ist:

  • Passwörter oder Sitzungstoken
  • Kreditkartennummern
  • Personenbezogene Daten über das Notwendige hinaus

Präventionsmuster:

  • Zentralisierte Protokollierung implementieren.
  • Warnmeldungen für verdächtige Muster einrichten.
  • Genügend Kontext für Ermittlungen einbeziehen (Zeitstempel, Benutzer, IP, Aktion).
  • Protokolle vor Manipulation schützen.

A10: Server-Side Request Forgery (SSRF)

Die Anwendung ruft vom Benutzer bereitgestellte URLs ohne Validierung ab und ermöglicht Angreifern den Zugriff auf interne Ressourcen.

Beispiel:

# BAD: Fetching user-provided URL
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
return requests.get(url).text # Can access internal services!

Präventionsmuster:

  • URL-Eingaben validieren und bereinigen.
  • Allowlists für erlaubte Domains verwenden.
  • Anfragen an interne IP-Bereiche blockieren (10.x, 172.16.x, 192.168.x, 169.254.x).
  • Netzwerksegmentierung verwenden, damit der Webserver keine internen Services erreichen kann.

Eingabevalidierung

Die meisten Schwachstellen betreffen nicht vertrauenswürdige Eingaben, die unsicher verarbeitet werden. Eingabevalidierung ist Ihre erste Verteidigungslinie.

Validierungsprinzipien

Auf dem Server validieren, nicht nur auf dem Client. Clientseitige Validierung verbessert die UX, bietet aber null Sicherheit. Jeder kann die JavaScript-Validierung umgehen, indem er Anfragen direkt sendet.

Wenn möglich gegen eine Allowlist validieren. Definieren Sie, was akzeptabel ist, und lehnen Sie alles andere ab.

# BAD: Blocklist approach (trying to block bad things)
if "<script>" in input:
reject()

# GOOD: Allowlist approach (only accept known good patterns)
if not re.match(r'^[a-zA-Z0-9_-]+$', username):
reject()

Typ, Länge, Format und Bereich validieren.

def validate_user_input(data):
errors = []

# Type validation
if not isinstance(data.get('age'), int):
errors.append("Age must be an integer")

# Range validation
if data.get('age', 0) < 0 or data.get('age', 0) > 150:
errors.append("Age must be between 0 and 150")

# Length validation
if len(data.get('username', '')) > 50:
errors.append("Username too long")

# Format validation
if not re.match(r'^[a-zA-Z0-9_]+$', data.get('username', '')):
errors.append("Username contains invalid characters")

return errors

Häufige Validierungsmuster

E-Mail-Adressen:

import re
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(email_pattern, email):
raise ValidationError("Invalid email format")

URLs:

from urllib.parse import urlparse

def validate_url(url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValidationError("Invalid URL scheme")
if not parsed.netloc:
raise ValidationError("Invalid URL")

Datei-Uploads:

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB

def validate_file(file):
# Check extension
ext = file.filename.rsplit('.', 1)[-1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValidationError(f"File type not allowed: {ext}")

# Check size
file.seek(0, 2) # Seek to end
size = file.tell()
file.seek(0) # Reset
if size > MAX_FILE_SIZE:
raise ValidationError("File too large")

# Check magic bytes (file signature)
# Don't trust the extension alone

Ausgabekodierung

Validierung verhindert, dass schlechte Daten in Ihr System gelangen. Ausgabekodierung verhindert, dass gespeicherte Daten in einem schädlichen Kontext ausgeführt werden.

Kontextspezifische Kodierung

HTML-Kontext:

# BAD: Raw output in HTML
return f"<p>Welcome, {username}</p>"

# GOOD: HTML-encode the output
from markupsafe import escape
return f"<p>Welcome, {escape(username)}</p>"

# Or use a template engine that auto-escapes (Jinja2, Django templates)

JavaScript-Kontext:

# BAD: User data in JavaScript
return f'<script>var name = "{username}";</script>'

# GOOD: JSON-encode for JavaScript context
import json
return f'<script>var name = {json.dumps(username)};</script>'

URL-Kontext:

# BAD: Raw value in URL
return f'<a href="/search?q={query}">Search</a>'

# GOOD: URL-encode
from urllib.parse import quote
return f'<a href="/search?q={quote(query)}">Search</a>'

SQL-Kontext: Verwenden Sie immer parametrisierte Abfragen. Keine Menge an Kodierung ersetzt die richtige Abfrageparametrisierung.

CSRF-Schutz

Cross-Site Request Forgery verleitet den Browser eines Benutzers dazu, unerwünschte Anfragen an eine Website zu senden, bei der er angemeldet ist. Wenn Sie bei Ihrer Bank angemeldet sind, könnte eine bösartige Website in Ihrem Namen eine Überweisung einreichen.

Wie CSRF funktioniert

1. User logs into bank.com, gets session cookie
2. User visits malicious-site.com (in another tab)
3. Malicious site contains: <img src="https://bank.com/transfer?to=attacker&amount=1000">
4. Browser sends request to bank.com WITH the session cookie
5. Bank processes the transfer — user is authenticated

CSRF-Tokens

Die Standardabwehr: Fügen Sie ein geheimes Token in Formulare ein, das der Angreifer nicht erraten kann.

Server generiert Token:

import secrets

def get_csrf_token(session):
if 'csrf_token' not in session:
session['csrf_token'] = secrets.token_hex(32)
return session['csrf_token']

Im Formular einbinden:

<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<!-- other fields -->
</form>

Bei der Übermittlung validieren:

@app.route('/transfer', methods=['POST'])
def transfer():
if request.form.get('csrf_token') != session.get('csrf_token'):
abort(403, 'Invalid CSRF token')
# Process the request

Framework-Unterstützung:

Die meisten Frameworks handhaben CSRF automatisch:

# Django - enabled by default
# Just include {% csrf_token %} in forms

# Flask-WTF
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
// Express.js with csurf
const csrf = require('csurf');
app.use(csrf({ cookie: true }));

app.get('/form', (req, res) => {
res.render('form', { csrfToken: req.csrfToken() });
});

SameSite-Cookies

Moderne Browser unterstützen das SameSite-Cookie-Attribut, das verhindert, dass Cookies mit Cross-Site-Anfragen gesendet werden.

# Set SameSite attribute
response.set_cookie(
'session',
value=session_id,
httponly=True,
secure=True,
samesite='Lax' # or 'Strict'
)

SameSite-Werte:

  • Strict — Cookie wird niemals mit Cross-Site-Anfragen gesendet. Am sichersten, bricht aber legitime Links von anderen Websites.
  • Lax — Cookie wird bei Navigation auf oberster Ebene (Links anklicken) gesendet, aber nicht bei eingebetteten Anfragen (Bilder, iFrames, AJAX). Gute Standardeinstellung.
  • None — Cookie wird mit allen Anfragen gesendet. Muss das Secure-Flag verwenden. Nur für legitime Cross-Site-Anwendungsfälle.

Empfehlung: Verwenden Sie SameSite=Lax plus CSRF-Token für Defense in Depth. Verlassen Sie sich nicht allein auf SameSite — ältere Browser unterstützen es nicht.

CSRF für APIs

Für APIs, die Token verwenden (keine Cookies), ist CSRF kein Problem — der Angreifer kann das Token nicht von einer anderen Website stehlen.

Wenn Ihre API jedoch Cookie-basierte Authentifizierung verwendet:

  • Einen benutzerdefinierten Header erfordern (z.B. X-Requested-With) — Browser senden keine benutzerdefinierten Header bei einfachen Cross-Origin-Anfragen
  • Oder das CSRF-Token in einem Header erfordern
// Frontend sends CSRF token in header
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken()
},
body: JSON.stringify(data)
});

API-Sicherheit

Wenn Sie REST-APIs bauen, gibt es zusätzliche Sicherheitsüberlegungen.

Rate Limiting

Ohne Rate Limiting können Angreifer:

  • Passwörter brute-forcen
  • Ihre Daten scrapen
  • Ihren Service zum Absturz bringen (DoS)
  • Ihre Infrastrukturkosten in die Höhe treiben

Express.js:

const rateLimit = require('express-rate-limit');

// General rate limit
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: 'Too many requests, please try again later'
});
app.use('/api/', limiter);

// Stricter limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
message: 'Too many login attempts'
});
app.use('/api/login', authLimiter);

Flask:

from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(app, key_func=get_remote_address)

@app.route('/api/login', methods=['POST'])
@limiter.limit("5 per hour")
def login():
# ...

@app.route('/api/data')
@limiter.limit("100 per minute")
def get_data():
# ...

Django:

# Using django-ratelimit
from django_ratelimit.decorators import ratelimit

@ratelimit(key='ip', rate='5/h', method='POST', block=True)
def login(request):
# ...

Rate-Limiting-Strategien:

  • Nach IP-Adresse (kann mit Proxys umgangen werden)
  • Nach Benutzerkonto (für authentifizierte Endpunkte)
  • Nach API-Schlüssel
  • Kombination aus dem Obigen

CORS-Konfiguration

CORS (Cross-Origin Resource Sharing) kontrolliert, welche Websites Ihre API von JavaScript aus aufrufen können.

Das Problem:

// From evil-site.com
fetch('https://your-api.com/user/data')
.then(r => r.json())
.then(data => sendToAttacker(data));

Ohne CORS-Header blockieren Browser das. Mit falsch konfiguriertem CORS können Angreifer Daten von authentifizierten Benutzern stehlen.

Schlechte Konfiguration:

# DON'T DO THIS - allows any site to access your API
@app.after_request
def add_cors(response):
response.headers['Access-Control-Allow-Origin'] = '*'
response.headers['Access-Control-Allow-Credentials'] = 'true' # DANGEROUS with *
return response

Gute Konfiguration:

# Flask-CORS
from flask_cors import CORS

# Allow only your frontend
CORS(app, origins=['https://your-frontend.com'], supports_credentials=True)
// Express.js
const cors = require('cors');

app.use(cors({
origin: 'https://your-frontend.com',
credentials: true
}));

CORS-Regeln:

  • Niemals Access-Control-Allow-Origin: * mit Access-Control-Allow-Credentials: true verwenden
  • Bestimmte Origins auf die Allowlist setzen, nicht blindlings den Origin-Header wiederspiegeln
  • Mit erlaubten Methoden und Headern restriktiv sein

API-Authentifizierung

API-Schlüssel:

@app.before_request
def check_api_key():
if request.path.startswith('/api/'):
api_key = request.headers.get('X-API-Key')
if not api_key or not validate_api_key(api_key):
abort(401)

API-Schlüssel-Best-Practices:

  • Lange, zufällige Schlüssel generieren (32+ Zeichen)
  • Gehasht in der Datenbank speichern (wie Passwörter)
  • Schlüsselrotation ohne Ausfallzeiten ermöglichen
  • Schlüsselnutzung für Audits protokollieren
  • Schlüsselbereiche/Berechtigungen implementieren

JWT-Sicherheit

JSON Web Tokens werden häufig für API-Authentifizierung verwendet. Sie werden auch häufig falsch konfiguriert.

Wie JWT funktioniert

Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxMjM0fQ.signature

Der Server signiert das Token mit einem geheimen Schlüssel. Der Client sendet es bei Anfragen zurück. Der Server verifiziert die Signatur, um der Nutzlast zu vertrauen.

Häufige JWT-Fehler

Fehler 1: Algorithmusverwirrung (alg:none)

Einige JWT-Bibliotheken akzeptieren alg: "none", was bedeutet, keine Signaturverifizierung.

# Attacker crafts token with no signature
# Header: {"alg": "none"}
# Payload: {"user_id": "admin"}
# Signature: (empty)

Lösung: Immer den Algorithmus explizit validieren:

import jwt

# BAD: Library might accept "none"
data = jwt.decode(token, SECRET_KEY)

# GOOD: Explicitly specify allowed algorithms
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])

Fehler 2: Schwache oder geleakte Secrets

# BAD
SECRET_KEY = 'secret' # Easily guessable

# GOOD
SECRET_KEY = os.environ.get('JWT_SECRET') # At least 256 bits of entropy

Fehler 3: In localStorage speichern

// BAD: XSS can steal the token
localStorage.setItem('token', jwt);

// BETTER: HttpOnly cookie (not accessible to JavaScript)
// Set from server with HttpOnly flag

Wenn Sie localStorage verwenden müssen, implementieren Sie zusätzliche Schutzmaßnahmen:

  • Kurze Token-Ablaufzeit
  • Token-Rotation
  • Fingerprinting

Fehler 4: Kein Ablaufdatum

# BAD: Token valid forever
payload = {'user_id': user.id}
token = jwt.encode(payload, SECRET_KEY)

# GOOD: Short expiration
from datetime import datetime, timedelta

payload = {
'user_id': user.id,
'exp': datetime.utcnow() + timedelta(hours=1)
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')

Fehler 5: Sensible Daten in der Nutzlast

JWTs sind kodiert, nicht verschlüsselt. Jeder kann die Nutzlast dekodieren.

# BAD: Don't put secrets in JWT
payload = {
'user_id': 123,
'credit_card': '4111111111111111' # Visible to anyone!
}

# GOOD: Only include non-sensitive identifiers
payload = {
'user_id': 123,
'roles': ['user']
}

JWT-Best-Practices

import jwt
from datetime import datetime, timedelta

def create_token(user_id, roles):
payload = {
'sub': user_id, # Subject
'roles': roles,
'iat': datetime.utcnow(), # Issued at
'exp': datetime.utcnow() + timedelta(hours=1), # Expiration
'jti': secrets.token_hex(16) # Unique token ID (for revocation)
}
return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_token(token):
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=['HS256'], # Explicit algorithm
options={'require': ['exp', 'sub']} # Required claims
)
return payload
except jwt.ExpiredSignatureError:
raise AuthError('Token expired')
except jwt.InvalidTokenError:
raise AuthError('Invalid token')

Vergleich der Token-Speicherorte

SpeicherortXSS-anfälligCSRF-anfälligEmpfehlung
localStorageJaNeinFür sensible Token vermeiden
sessionStorageJaNeinEtwas besser (beim Tab-Schließen gelöscht)
HttpOnly-CookieNeinJa (mit SameSite abschwächen)Beste Wahl für Webanwendungen
Nur im SpeicherNeinNeinBeste Sicherheit, geht bei Aktualisierung verloren

Für die meisten Webanwendungen: HttpOnly-Cookies mit SameSite=Lax verwenden.

Für SPAs, die Token-Zugriff benötigen: Kurzlebige Zugriffstoken im Speicher + HttpOnly-Refresh-Token in Cookies verwenden.

Sicherer Umgang mit Secrets

Secrets im Quellcode gehören zu den häufigsten Sicherheitsproblemen in kleinen Unternehmen.

Was als Secret gilt

  • API-Schlüssel
  • Datenbankpasswörter
  • Verschlüsselungsschlüssel
  • OAuth-Client-Secrets
  • JWT-Signing-Keys
  • Service-Account-Zugangsdaten
  • Private SSH-Schlüssel

Wo Secrets NICHT sein sollten

  • Quellcode-Dateien
  • Git-Repositories (auch nach dem Löschen — die Git-Historie bewahrt sie)
  • Konfigurationsdateien, die in die Versionskontrolle eingecheckt werden
  • Protokolldateien
  • Fehlermeldungen
  • Clientseitiger Code (JavaScript, Mobile Apps)

Wo Secrets SEIN sollten

Umgebungsvariablen:

# config.py
import os

DATABASE_URL = os.environ.get('DATABASE_URL')
API_KEY = os.environ.get('API_KEY')

Secrets Manager (empfohlen):

# Using Passwork CLI or API
import subprocess
import json

def get_secret(secret_id):
result = subprocess.run(
['passwork-cli', 'get', secret_id, '--format', 'json'],
capture_output=True
)
return json.loads(result.stdout)['password']

DATABASE_PASSWORD = get_secret('database-production')

Cloud-Secrets-Manager:

# AWS Secrets Manager
import boto3

def get_secret(secret_name):
client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId=secret_name)
return response['SecretString']

Secrets im Code prüfen

Verwenden Sie automatisierte Tools, um nach versehentlich eingecheckten Secrets zu scannen:

# Install git-secrets
brew install git-secrets

# Add common patterns
git secrets --register-aws

# Scan repository
git secrets --scan

# Install as pre-commit hook
git secrets --install

Weitere Tools:

  • truffleHog — Scannt die Git-Historie nach Zeichenketten mit hoher Entropie
  • gitleaks — Schnelles Scannen nach Secrets in Git-Repos
  • detect-secrets — Yelps Tool zur Verhinderung von Secrets im Code

Häufige Schwachstellen nach Programmiersprache

JavaScript/Node.js

Prototype Pollution:

// BAD: Merging user input into objects
Object.assign(config, userInput);

// GOOD: Create new object, validate keys
const sanitized = {};
for (const key of allowedKeys) {
if (key in userInput) {
sanitized[key] = userInput[key];
}
}

eval() und Function():

// BAD: Never use eval with user input
eval(userInput);

// GOOD: Use JSON.parse for data, avoid eval entirely
const data = JSON.parse(userInput);

NoSQL-Injection (MongoDB):

// BAD: Query from user input
db.users.find({ username: req.body.username });
// Attacker sends: { "$gt": "" } to match all users

// GOOD: Explicitly expect a string
const username = String(req.body.username);
db.users.find({ username: username });

Python

Unsichere Deserialisierung:

# BAD: pickle with untrusted data
import pickle
data = pickle.loads(user_input) # Can execute arbitrary code!

# GOOD: Use JSON
import json
data = json.loads(user_input)

Template-Injection:

# BAD: User input in template string
from jinja2 import Template
Template(user_input).render() # SSTI vulnerability

# GOOD: User input as template variable
Template("Hello {{ name }}").render(name=user_input)

PHP

Local File Inclusion:

// BAD: User input in include
include($_GET['page'] . '.php');

// GOOD: Allowlist of permitted pages
$allowed = ['home', 'about', 'contact'];
if (in_array($_GET['page'], $allowed)) {
include($_GET['page'] . '.php');
}

Type Juggling:

// BAD: Loose comparison with user input
if ($password == $user_password) { ... }

// GOOD: Strict comparison
if ($password === $user_password) { ... }

// BEST: Use password_verify for passwords
if (password_verify($password, $hash)) { ... }

Java

XML External Entity (XXE):

// BAD: Default XML parser settings
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document doc = db.parse(userInput);

// GOOD: Disable external entities
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
DocumentBuilder db = dbf.newDocumentBuilder();

Unsichere Deserialisierung:

// BAD: Deserializing untrusted data
ObjectInputStream ois = new ObjectInputStream(userInputStream);
Object obj = ois.readObject(); // Can execute arbitrary code

// GOOD: Use JSON or validate object types

Sicherheits-Header

Konfigurieren Sie Ihren Webserver oder Ihre Anwendung so, dass Sicherheits-Header gesendet werden:

# Content Security Policy - controls what resources can load
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'

# Prevent clickjacking
X-Frame-Options: DENY

# Prevent MIME type sniffing
X-Content-Type-Options: nosniff

# Enable browser XSS filter
X-XSS-Protection: 1; mode=block

# Force HTTPS
Strict-Transport-Security: max-age=31536000; includeSubDomains

# Control referrer information
Referrer-Policy: strict-origin-when-cross-origin

# Prevent browser features you don't use
Permissions-Policy: geolocation=(), microphone=(), camera=()

Express.js-Beispiel:

const helmet = require('helmet');
app.use(helmet());

Django-Beispiel:

# settings.py
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000

Checkliste für sicheres Programmieren

Verwenden Sie diese während Code-Reviews:

Authentifizierung und Autorisierung

  • Passwörter mit bcrypt/Argon2 gehasht (nicht MD5/SHA1)
  • Sitzungstoken sind zufällig, lang und HTTP-only
  • Autorisierung wird bei jeder Anfrage geprüft
  • Fehlgeschlagene Anmeldeversuche werden rate-limitiert
  • Passwort-Reset-Token laufen ab

Eingabe und Ausgabe

  • Alle Eingaben werden auf dem Server validiert
  • Validierung verwendet wenn möglich Allowlists
  • SQL verwendet parametrisierte Abfragen
  • HTML-Ausgabe wird kodiert
  • Datei-Uploads werden validiert (Typ, Größe, Inhalt)

Secrets

  • Keine Secrets im Quellcode
  • Secrets werden aus der Umgebung oder dem Secrets Manager geladen
  • API-Schlüssel haben die minimal erforderlichen Berechtigungen
  • Secrets können ohne Code-Deployment rotiert werden

Konfiguration

  • Debug-Modus in der Produktion deaktiviert
  • Fehlermeldungen legen keine internen Details offen
  • Sicherheits-Header konfiguriert
  • HTTPS erzwungen

Abhängigkeiten

  • Abhängigkeiten aktuell
  • Keine bekannten anfälligen Versionen
  • Ungenutzte Abhängigkeiten entfernt

Workshop: Ihre Codebasis sichern

Blocken Sie 2–3 Stunden für diese Bewertung und erste Korrekturen.

Teil 1: Secrets-Scan (30 Minuten)

  1. Einen Secrets-Scanner installieren:
pip install trufflehog
# or
brew install gitleaks
  1. Ihr Repository scannen:
trufflehog git file://. --only-verified
# or
gitleaks detect --source .
  1. Alle Befunde dokumentieren
  2. Exponierte Secrets sofort rotieren
  3. Secrets aus Code und Git-Historie entfernen, falls nötig

Ergebnis: Liste der gefundenen Secrets und Behebungsstatus

Teil 2: Abhängigkeits-Audit (30 Minuten)

  1. Das Audit-Tool Ihrer Sprache ausführen:
# Node.js
npm audit

# Python
pip-audit

# Ruby
bundle audit

# PHP
composer audit
  1. Anfällige Pakete dokumentieren
  2. Was sofort aktualisiert werden kann, aktualisieren
  3. Tickets für Updates erstellen, die Tests erfordern

Ergebnis: Bericht über Abhängigkeitsschwachstellen

Teil 3: Code-Review für OWASP Top 10 (60 Minuten)

Wählen Sie 3–5 kritische Endpunkte (Login, Registrierung, Passwort-Reset, Datenzugriff) und prüfen Sie auf:

  • SQL-Injection
  • XSS-Schwachstellen
  • Fehlende Autorisierungsprüfungen
  • Unsichere direkte Objektreferenzen
  • Exposition sensibler Daten

Ergebnis: Liste der Befunde mit Schweregradbewertungen

Teil 4: Sicherheits-Header-Prüfung (15 Minuten)

  1. Besuchen Sie securityheaders.com
  2. Ihre Produktions-URL scannen
  3. Fehlende Header dokumentieren
  4. Fehlende Header implementieren oder Ticket erstellen

Ergebnis: Sicherheits-Header-Bericht und Behebungsplan

Teil 5: Team-Checkliste erstellen (30 Minuten)

  1. Die obige Checkliste für sicheres Programmieren an Ihren Stack anpassen
  2. Sprachspezifische Punkte hinzufügen
  3. Mit dem Team teilen
  4. Zum Code-Review-Prozess hinzufügen

Ergebnis: Team-spezifische Checkliste für sicheres Programmieren

Häufige Fehler

Fehler 1: Sicherheit durch Verschleierung

Dinge verstecken (nicht standardmäßige Ports, verschleierte URLs) statt sie zu sichern. Verschleierung erhöht den Aufwand für Angreifer, ist aber keine echte Sicherheit.

Lösung: By Design sichern. Angenommen, Angreifer wissen alles über Ihr System außer Secrets.

Fehler 2: Nur clientseitige Validierung

Darauf vertrauen, dass JavaScript-Validierung schlechte Eingaben verhindert.

Lösung: Immer auf dem Server validieren. Clientseitige Validierung ist nur für die UX.

Fehler 3: DIY-Kryptografie

Eigene Verschlüsselung, Hashing oder Token-Generierung schreiben.

Lösung: Etablierte Bibliotheken verwenden. bcrypt für Passwörter verwenden. Das Sitzungsmanagement Ihres Frameworks nutzen. Standard-JWT-Bibliotheken verwenden.

Fehler 4: Fehlermeldungen, die Angreifern helfen

„Ungültiger Benutzername" vs. „Ungültiges Passwort" verrät Angreifern, welche Benutzernamen existieren.

Lösung: Generische Fehlermeldungen: „Ungültige Zugangsdaten."

Fehler 5: Secrets protokollieren

Anfrage-Inhalte in Protokolle ausgeben, einschließlich Passwörtern und API-Schlüsseln.

Lösung: Sensible Felder vor dem Protokollieren filtern. Niemals Passwörter, Token oder Schlüssel protokollieren.

Tools für sichere Entwicklung

Statische Analyse (SAST)

ToolSprachenKostenlose Version
SemgrepVieleJa
BanditPythonOpen Source
BrakemanRuby/RailsOpen Source
ESLint security pluginsJavaScriptOpen Source
SonarQubeVieleCommunity-Edition
CodeQLVieleKostenlos für Open Source

Secrets-Scanning

ToolBeschreibung
git-secretsAWS-Tool, Pre-Commit-Hook
gitleaksSchnell, konfigurierbar
truffleHogTiefes Scanning, verifizierte Secrets
detect-secretsYelps Tool, gut für CI

Abhängigkeits-Scanning

ToolÖkosystem
npm auditNode.js
pip-auditPython
bundler-auditRuby
SnykMehrere, kostenlose Version
DependabotGitHub, mehrere

Erklärung gegenüber der Führungsebene

Wenn jemand fragt, warum Sie Zeit mit sicherem Programmieren verbringen:

„Ich überprüfe unseren Code auf häufige Sicherheitsschwachstellen — dieselben Probleme, die die meisten Datenpannen verursachen. Dinge wie SQL-Injection und Cross-Site-Scripting sind seit Jahrzehnten bekannt, tauchen aber ständig in neuem Code auf. Außerdem scanne ich nach versehentlich in unser Repository eingecheckten Secrets und prüfe unsere Abhängigkeiten auf bekannte Schwachstellen. Das reduziert das Risiko einer Datenpanne und erspart uns teurere Korrekturen später."

Kurze Version: „Ich finde und behebe Sicherheitsprobleme in unserem Code, bevor Angreifer sie finden."

Selbstcheck

Code-Sicherheit

  • OWASP Top 10 verstanden
  • SQL-Injection-Schwachstellen identifizieren können
  • XSS-Schwachstellen identifizieren können
  • Wissen, wie Passwörter richtig gehasht werden
  • Wissen, wie Eingaben validiert werden

Secrets-Verwaltung

  • Repository nach Secrets gescannt
  • Keine Secrets im Quellcode
  • Secrets werden aus der Umgebung oder dem Secrets Manager geladen
  • Pre-Commit-Hook zur Verhinderung neuer Secrets

Abhängigkeiten

  • Abhängigkeits-Audit durchgeführt
  • Keine kritischen Schwachstellen in Abhängigkeiten
  • Prozess zur Aktualisierung von Abhängigkeiten

Prozess

  • Checkliste für sicheres Programmieren vorhanden
  • Sicherheit wird im Code-Review berücksichtigt
  • Statisches Analyse-Tool vorhanden (oder geplant)

Haken Sie mindestens 10 von 14 Punkten ab, bevor Sie weitermachen.

Was als nächstes kommt

Sie verstehen die häufigen Schwachstellen und wie Sie sie im Code verhindern. Nächstes Kapitel: Verwaltung von Sicherheitsanforderungen — wie Sie Sicherheitsanforderungen mit OWASP ASVS definieren, verfolgen und verifizieren und in Ihren Entwicklungsworkflow integrieren.