1) Why ML APIs need special security controls
A deployed ML API is not just another REST endpoint. It can be attacked to:
- evade detection (adversarial inputs)
- extract the model (model stealing via queries)
- leak training data (membership inference / inversion)
- poison feedback loops (if API outputs feed retraining)
- cause denial-of-service (GPU exhaustion, large payloads)
So “secure API endpoints” means traditional API security + ML-specific protections.
2) API attack surfaces (what to defend)
A) Input abuse
- adversarial examples crafted via black-box probing
- malformed payloads (parser crashes)
- extreme sizes (huge images, long text) → DoS
B) Output leakage
- returning probabilities/logits helps attackers
- detailed explanations can reveal sensitive logic/features
C) Query-based attacks
- model extraction / distillation
- membership inference using confidence patterns
- boundary probing with repeated near-duplicate requests
D) Infrastructure exploitation
- auth bypass, key leakage
- insecure model artifact loading
- weak rate limits → resource exhaustion
3) Secure API controls (the “must-have” stack)
3.1 Authentication and authorization (AuthN/AuthZ)
Goal: only approved clients can call the API, and only within allowed scopes.
Best practices:
- Use strong authentication (OAuth2/JWT, mTLS for service-to-service)
- Separate user tokens from service tokens
- Enforce scopes/roles:
predict:basicpredict:with_explanationsadmin:metrics
- Rotate secrets and use short-lived tokens
- Never embed long-lived keys in mobile apps (use a backend broker)
3.2 Rate limiting + quotas (anti-probing and anti-DoS)
Goal: limit attacker ability to brute-force the decision boundary or extract the model.
Controls:
- per-IP, per-user, per-API-key limits
- burst controls + sustained rate controls
- daily quotas per tenant
- exponential backoff and temporary bans on abuse signals
ML-specific tip:
- create separate limits for expensive endpoints (image, LLM calls)
- throttle “probability outputs” endpoints more aggressively
3.3 Strict input validation + normalization
Goal: reject unsafe inputs before they hit model/GPU.
Controls:
- schema validation (types, required fields)
- file type allowlist (jpg/png), reject polyglot files
- max payload size (bytes) and max dimensions
- text length caps, token limits, unicode normalization
- canonicalization (e.g., normalize whitespace, decode safely)
- quarantine suspicious inputs for analysis
3.4 Output shaping (reduce leakage)
Goal: reduce information that helps attackers.
Safer defaults:
- return top-1 label (or top-k with coarse scores)
- avoid returning raw logits
- bucket confidence into bands (e.g., “high/medium/low”)
- disable explanations by default; gate behind role/scopes
Privacy defense:
- avoid deterministic high-precision probabilities for sensitive domains
- consider adding minimal noise or calibration controls where appropriate
3.5 Abuse detection and anomaly monitoring
Goal: detect probing, extraction, and adversarial behavior.
Signals:
- repeated near-duplicate inputs
- high-frequency queries with small changes
- sudden shift in confidence distributions
- high error-rate bursts
- suspicious geography/IP churn
Actions:
- step-up auth (captcha / re-auth)
- throttle or block
- route to a “safe model” or reject with generic response
3.6 Secure logging without leaking PII
Goal: maintain auditability without creating new privacy risks.
Best practices:
- log request metadata + hashed IDs (not raw PII)
- store minimal sample payloads; if needed, encrypt and restrict access
- sanitize logs to prevent log injection
- keep security logs separate from analytics logs
3.7 Secure deployment hygiene (infrastructure)
Goal: protect model server and dependencies.
Controls:
- isolate model inference service in a restricted network segment
- run as non-root; minimal container images
- block outbound network access unless needed
- patch model server dependencies (CVE scanning)
- load model artifacts from signed, trusted storage only
4) Defense-in-depth patterns for ML APIs
Pattern A — Two-tier inference
- Public endpoint: limited outputs, strict throttles
- Internal endpoint: richer outputs (scores/explanations), used only by trusted services
Pattern B — Risk-based routing
- low-risk requests → standard model
- suspicious requests → hardened model / extra preprocessing / reject
Pattern C — Shadow evaluation
- log suspicious traffic to a shadow pipeline for red-team analysis
- do not feed it into retraining until validated
5) ELMS-ready security checklist for ML APIs
Access
- OAuth2/JWT or mTLS enabled
- Least privilege scopes/roles
- Secret rotation + short-lived tokens
Traffic controls
- Rate limits per IP/key/tenant
- Quotas for expensive endpoints
- DoS protections at gateway/WAF level
Input controls
- Schema validation + size caps
- File type allowlist + safe decoding
- Unicode normalization + text length limits
Output controls
- No logits by default
- Confidence bucketing or restricted top-k
- Explanations gated behind privileged scopes
Monitoring
- Probing detection alerts
- Drift + confidence shift dashboards
- Abuse response runbook (block/throttle/rollback)
Supply chain
- Signed model artifacts + provenance
- Dependency scanning + patching cadence
6) Code patterns (FastAPI examples)
6.1 Input schema validation + size checks (FastAPI)
from fastapi import FastAPI, Request, HTTPException, Depends
from pydantic import BaseModel, Field, conlist
import time
app = FastAPI()
MAX_TEXT_LEN = 2000
class PredictRequest(BaseModel):
text: str = Field(..., min_length=1, max_length=MAX_TEXT_LEN)
metadata: dict | None = None
@app.post("/predict")
async def predict(req: PredictRequest, request: Request):
# Basic validation already enforced by Pydantic
# Add extra gate checks if needed (e.g., deny suspicious encodings)
if "\x00" in req.text:
raise HTTPException(status_code=400, detail="Invalid text input")
# TODO: call model safely
pred_label = "SAFE"
confidence_band = "MEDIUM" # avoid raw probability
return {"label": pred_label, "confidence": confidence_band}
6.2 Simple rate limiting (in-memory example)
For production, prefer API gateway/WAF (NGINX/Envoy/Kong/Cloudflare) or Redis-backed limiter.
from collections import defaultdict, deque
WINDOW_SEC = 10
MAX_REQ_PER_WINDOW = 30
req_log = defaultdict(deque)
def rate_limit(ip: str):
now = time.time()
q = req_log[ip]
while q and q[0] < now - WINDOW_SEC:
q.popleft()
if len(q) >= MAX_REQ_PER_WINDOW:
raise HTTPException(status_code=429, detail="Rate limit exceeded")
q.append(now)
@app.post("/predict_limited")
async def predict_limited(req: PredictRequest, request: Request):
ip = request.client.host
rate_limit(ip)
return {"label": "SAFE", "confidence": "LOW"}
6.3 Output shaping: avoid leaking logits/probabilities
def bucket_confidence(p: float) -> str:
if p >= 0.9:
return "HIGH"
if p >= 0.7:
return "MEDIUM"
return "LOW"
# Return only bucketed confidence, not raw p
return {"label": pred_label, "confidence": bucket_confidence(max_prob)}


Comments are closed.