API Security

Von Boris Sander16. November 2025
API Security

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)

RangVulnerabilityBeschreibung
API1Broken Object Level AuthorizationZugriff auf fremde Objekte via ID
API2Broken AuthenticationSchwache Auth-Mechanismen
API3Broken Object Property Level AuthorizationZugriff auf nicht-autorisierte Properties
API4Unrestricted Resource ConsumptionDoS durch fehlende Rate Limits
API5Broken Function Level AuthorizationZugriff auf Admin-Funktionen
API6Unrestricted Access to Sensitive Business FlowsMissbrauch von Business-Logik
API7Server Side Request ForgerySSRF-Angriffe
API8Security MisconfigurationFehlkonfiguration
API9Improper Inventory ManagementUnbekannte/alte API-Endpoints
API10Unsafe Consumption of APIsUnsichere 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

ToolTypFokus
OWASP ZAPDASTAutomatisiertes Scanning
Burp SuiteProxyManuelles Testing
PostmanFunctionalCollection Testing
DreddContractOpenAPI Validation
NucleiScannerTemplate-basiert
42CrunchSpec-basedOpenAPI 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: