API Security
„Deine API ist kein geheimer Hintereingang. Jeder mit einem Browser kann sie sehen."
Sinn & Zweck
APIs sind das Rückgrat moderner Anwendungen. Mobile Apps, SPAs, Microservices, Third-Party-Integrationen – alles kommuniziert über APIs. Damit sind APIs auch das primäre Angriffsziel.
Das Problem:
- APIs exponieren Geschäftslogik direkt
- Keine Browser-Security (SOP, CSP) bei Mobile Apps
- Entwickler denken "API = Backend = sicher"
- Dokumentation verrät Angreifern alles
API Security bedeutet: Die API so absichern, dass nur autorisierte Clients die richtigen Daten bekommen – und nichts mehr.
OWASP API Security Top 10 (2023)
| Rang | Vulnerability | Beschreibung |
|---|---|---|
| API1 | Broken Object Level Authorization | Zugriff auf fremde Objekte via ID |
| API2 | Broken Authentication | Schwache Auth-Mechanismen |
| API3 | Broken Object Property Level Authorization | Zugriff auf nicht-autorisierte Properties |
| API4 | Unrestricted Resource Consumption | DoS durch fehlende Rate Limits |
| API5 | Broken Function Level Authorization | Zugriff auf Admin-Funktionen |
| API6 | Unrestricted Access to Sensitive Business Flows | Missbrauch von Business-Logik |
| API7 | Server Side Request Forgery | SSRF-Angriffe |
| API8 | Security Misconfiguration | Fehlkonfiguration |
| API9 | Improper Inventory Management | Unbekannte/alte API-Endpoints |
| API10 | Unsafe Consumption of APIs | Unsichere Third-Party-API-Nutzung |
Die größten Probleme im Detail
API1: Broken Object Level Authorization (BOLA/IDOR)
Das Problem:
# User A ruft seine Daten ab
GET /api/users/123/orders
Authorization: Bearer token_of_user_A
# User A ändert die ID und sieht User Bs Daten
GET /api/users/456/orders ← Fremde Daten!
Authorization: Bearer token_of_user_A
Die Lösung:
# JEDE Ressourcen-Anfrage prüfen
@app.get("/api/users/{user_id}/orders")
def get_orders(user_id: int, current_user: User = Depends(get_current_user)):
if current_user.id != user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="Not authorized")
return db.get_orders(user_id)
API2: Broken Authentication
Schwachstellen:
weak_authentication:
- Keine Rate Limiting bei Login
- Schwache Password-Policies
- JWT ohne Expiration
- Credentials in URL (GET-Parameter)
- Keine MFA-Option
- Token ohne ordentliche Validierung
Best Practice:
# JWT mit korrekten Claims
def create_access_token(user_id: int) -> str:
return jwt.encode({
"sub": str(user_id),
"exp": datetime.utcnow() + timedelta(minutes=15),
"iat": datetime.utcnow(),
"jti": str(uuid.uuid4()), # Unique Token ID
}, SECRET_KEY, algorithm="HS256")
# Validierung
def validate_token(token: str) -> dict:
try:
payload = jwt.decode(
token,
SECRET_KEY,
algorithms=["HS256"],
options={"require": ["exp", "sub", "iat"]}
)
# Token-Revocation prüfen
if is_token_revoked(payload["jti"]):
raise HTTPException(status_code=401)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
API4: Unrestricted Resource Consumption
Das Problem:
# Angreifer fragt Millionen von Datensätzen ab
GET /api/users?limit=10000000
# Oder schickt riesige Payloads
POST /api/upload
Content-Length: 10737418240 # 10 GB
Die Lösung:
# Rate Limiting
from slowapi import Limiter
limiter = Limiter(key_func=get_remote_address)
@app.get("/api/users")
@limiter.limit("100/minute")
def list_users(limit: int = Query(default=20, le=100)):
# Limit auf max 100 enforced
return db.get_users(limit=limit)
# Request Size Limit
@app.post("/api/upload")
async def upload(file: UploadFile):
if file.size > 10 * 1024 * 1024: # 10 MB
raise HTTPException(413, "File too large")
API6: Unrestricted Access to Sensitive Business Flows
Das Problem:
# Ticket-Kauf ohne Schutz
@app.post("/api/tickets/purchase")
def purchase_ticket(ticket_id: int, current_user: User):
# Bot kauft alle Tickets in Millisekunden
ticket = db.get_ticket(ticket_id)
if ticket.available:
db.purchase(ticket, current_user)
return {"success": True}
Die Lösung:
# Anti-Automation Maßnahmen
@app.post("/api/tickets/purchase")
@limiter.limit("5/minute") # Rate Limit
def purchase_ticket(
ticket_id: int,
current_user: User,
captcha_token: str # CAPTCHA erforderlich
):
# CAPTCHA verifizieren
if not verify_captcha(captcha_token):
raise HTTPException(400, "Invalid captcha")
# Device Fingerprinting prüfen
if is_suspicious_device(request):
raise HTTPException(429, "Suspicious activity")
# Zeitliche Constraints
if user_purchased_recently(current_user, timedelta(minutes=1)):
raise HTTPException(429, "Please wait before purchasing again")
# Transaktion durchführen
return process_purchase(ticket_id, current_user)
Authentication & Authorization
OAuth 2.0 / OpenID Connect
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ Client │────→│ Auth Server │────→│ Resource │
│ (App) │ │ (IdP) │ │ Server │
└──────────┘ └──────────────┘ └──────────┘
│ │ │
│ 1. Auth Request │ │
│──────────────────→│ │
│ │ │
│ 2. User Login │ │
│ │ │
│ 3. Auth Code │ │
│←──────────────────│ │
│ │ │
│ 4. Exchange Code │ │
│──────────────────→│ │
│ │ │
│ 5. Access Token │ │
│←──────────────────│ │
│ │ │
│ 6. API Request + Token │
│─────────────────────────────────────→│
│ │
│ 7. Validate Token (introspection/JWKS)
│ │
│ 8. Response │
│←─────────────────────────────────────│
API Keys
use_cases:
- Machine-to-Machine
- Third-Party Integrations
- Rate Limiting per Client
security_requirements:
- Lange, zufällige Keys (min. 32 Zeichen)
- Hashen vor Speicherung
- Rotation ermöglichen
- Scope-Beschränkung
- Ablaufdatum
Beispiel:
# API Key Generation
import secrets
def generate_api_key() -> str:
return f"sk_{secrets.token_urlsafe(32)}"
# Validierung
def validate_api_key(key: str) -> APIKeyInfo:
key_hash = hashlib.sha256(key.encode()).hexdigest()
api_key = db.get_api_key_by_hash(key_hash)
if not api_key or api_key.revoked:
raise HTTPException(401, "Invalid API key")
if api_key.expires_at and api_key.expires_at < datetime.utcnow():
raise HTTPException(401, "API key expired")
return api_key
JWT Best Practices
jwt_requirements:
algorithm: RS256 (asymmetric) oder HS256 (symmetric, rotatable)
required_claims:
- exp: Ablaufzeit (kurz! 15 min für Access Token)
- iat: Ausstellungszeit
- sub: Subject (User ID)
- iss: Issuer
- aud: Audience
forbidden:
- algorithm: none
- sensitive_data: in payload (ist nicht verschlüsselt!)
- long_lived: Access Token > 1 Stunde
Input Validation & Output Encoding
Input Validation
from pydantic import BaseModel, Field, validator
from typing import Optional
class CreateUserRequest(BaseModel):
username: str = Field(..., min_length=3, max_length=50, regex="^[a-zA-Z0-9_]+$")
email: str = Field(..., regex=r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
age: Optional[int] = Field(None, ge=0, le=150)
@validator('username')
def username_no_reserved(cls, v):
reserved = ['admin', 'root', 'system']
if v.lower() in reserved:
raise ValueError('Reserved username')
return v
# Automatische Validierung
@app.post("/api/users")
def create_user(user: CreateUserRequest):
# user ist bereits validiert
return db.create_user(user)
Output Filtering
# Nicht alles zurückgeben!
class UserResponse(BaseModel):
id: int
username: str
email: str
# NICHT: password_hash, internal_notes, etc.
class Config:
orm_mode = True
@app.get("/api/users/{user_id}", response_model=UserResponse)
def get_user(user_id: int):
user = db.get_user(user_id)
# Pydantic filtert automatisch
return user
Rate Limiting
Strategien
rate_limiting:
global:
limit: 1000/minute
per_endpoint:
/api/login:
limit: 5/minute
lockout_after: 10 failed attempts
/api/users:
limit: 100/minute
/api/search:
limit: 30/minute # Teurer Endpoint
per_user:
authenticated: 500/minute
unauthenticated: 50/minute
Implementation (Redis-basiert)
import redis
from fastapi import HTTPException
r = redis.Redis()
def rate_limit(key: str, limit: int, window: int = 60):
current = r.get(key)
if current and int(current) >= limit:
raise HTTPException(429, "Rate limit exceeded")
pipe = r.pipeline()
pipe.incr(key)
pipe.expire(key, window)
pipe.execute()
# Verwendung
@app.get("/api/data")
def get_data(request: Request):
client_ip = request.client.host
rate_limit(f"rate:{client_ip}:data", limit=100)
return {"data": "..."}
API Gateway Security
┌──────────────────────────────────────────────────────────────────┐
│ API Gateway │
├──────────────────────────────────────────────────────────────────┤
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Auth │ │ Rate │ │ WAF │ │ Logging │ │
│ │ Validation │ │ Limiting │ │ │ │ │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ └───────────────┴───────────────┴───────────────┘ │
│ │ │
└───────────────────────────────┼──────────────────────────────────┘
│
┌──────────────────────┼──────────────────────┐
↓ ↓ ↓
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Service │ │ Service │ │ Service │
│ A │ │ B │ │ C │
└───────────┘ └───────────┘ └───────────┘
Kong / AWS API Gateway
# Kong Rate Limiting Plugin
plugins:
- name: rate-limiting
config:
minute: 100
policy: redis
- name: jwt
config:
claims_to_verify:
- exp
- name: cors
config:
origins:
- https://app.example.com
methods:
- GET
- POST
headers:
- Authorization
- Content-Type
API Security Testing
Tools
| Tool | Typ | Fokus |
|---|---|---|
| OWASP ZAP | DAST | Automatisiertes Scanning |
| Burp Suite | Proxy | Manuelles Testing |
| Postman | Functional | Collection Testing |
| Dredd | Contract | OpenAPI Validation |
| Nuclei | Scanner | Template-basiert |
| 42Crunch | Spec-based | OpenAPI Security |
Automatisierte Checks
# API Security Checklist
authentication:
- [ ] Starke Passwort-Policies
- [ ] MFA verfügbar
- [ ] Token-Expiration kurz
- [ ] Sichere Token-Storage (keine Cookies ohne HttpOnly)
authorization:
- [ ] BOLA-Tests für alle Endpoints
- [ ] Role-based Access Tests
- [ ] Admin-Funktion-Tests
input_validation:
- [ ] SQL Injection Tests
- [ ] XSS Tests
- [ ] XXE Tests
- [ ] Parameter Tampering
rate_limiting:
- [ ] Rate Limits vorhanden
- [ ] Keine Bypass möglich
- [ ] Angemessene Limits
data_exposure:
- [ ] Keine sensiblen Daten in Responses
- [ ] Keine Debug-Info in Production
- [ ] Error Messages nicht zu detailliert
Best Practices
1. Use TLS Everywhere
tls:
minimum_version: TLS 1.2
preferred_version: TLS 1.3
hsts: enabled
certificate_pinning: for_mobile_apps
2. Versionierung
# URL-basiert
GET /api/v1/users
# Header-basiert
GET /api/users
Accept: application/vnd.api.v1+json
3. Fehlerbehandlung
# SCHLECHT: Zu viele Details
{
"error": "SQL Error: SELECT * FROM users WHERE id = '1 OR 1=1'",
"stack_trace": "..."
}
# GUT: Generisch
{
"error": "Invalid request",
"error_code": "INVALID_INPUT",
"request_id": "abc-123" # Für Support-Lookup
}
4. CORS richtig konfigurieren
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com"], # NICHT: "*"
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE"],
allow_headers=["Authorization", "Content-Type"],
)
5. API Dokumentation sichern
# OpenAPI/Swagger nur intern
swagger_ui:
enabled: false # In Production
# Oder mit Auth
@app.get("/docs", dependencies=[Depends(admin_required)])
def get_docs():
return get_swagger_ui_html(...)
Fazit
API Security ist fundamental anders als klassische Web-Security. Kein Browser schützt dich mit Same-Origin-Policy, keine Cookies mit HttpOnly. Du bist auf dich allein gestellt – mit Authentication, Authorization, Validation und Monitoring.
Deine API ist keine Schnittstelle. Sie ist eine Angriffsfläche. Behandle sie entsprechend.
Weiterführende Ressourcen: