Localhost CSRF Protection
apfel's HTTP server now rejects cross-origin requests by default, protecting against the same localhost CSRF vulnerability class that hit Ollama (same port 11434!), Jupyter, and 175,000+ exposed LLM instances.
The problem: Any website you visit can silently fetch() to 127.0.0.1:11434. Without protection, a malicious page could abuse your on-device model for compute or exfiltrate responses (with --cors).
The fix: One middleware, zero breaking changes. curl, Python SDKs, and scripts keep working exactly as before - they don't send Origin headers.
What changed
Default behavior (apfel --serve):
- Requests without an
Originheader: allowed (curl, SDKs, scripts - unchanged) - Requests with localhost
Origin(127.0.0.1,localhost,[::1], any port): allowed - Requests with foreign
Origin(http://evil.com): rejected with 403 - Subdomain attacks (
http://localhost.evil.com): rejected - safe prefix matching
New flags:
| Flag | Description |
|---|---|
--allowed-origins <origins>
| Add comma-separated origins to localhost defaults |
--no-origin-check
| Disable origin checking (allow all origins) |
--token <secret>
| Require Bearer token authentication |
--token-auto
| Generate and print a random Bearer token |
--footgun
| Disable all protections (--no-origin-check + --cors)
|
New env var: APFEL_TOKEN - set Bearer token via environment.
Security details
- Origin check: Validates the
OriginHTTP header (only sent by browsers on cross-origin requests). No header = no check = backward compatible. - Token auth: Optional Bearer token for endpoints (except
/healthwhich stays open for monitoring). Explicit secrets from--tokenandAPFEL_TOKENare never echoed in the startup banner. - CORS consolidation: Previously scattered across 3 code locations, now centralized in a single
SecurityMiddleware. CORS headers are properly gated behind--cors(previously the OPTIONS handler always returned them). - Proper CORS: When
--corsis enabled with specific origins, responds with the actual request origin (not wildcard*) and includesVary: Origin. - 401 responses include
WWW-Authenticate: Bearerheader per RFC 7235. - Severity: Low - Apple's on-device model cannot access filesystem or network. Worst case is compute abuse or response exfiltration with
--cors.
Architecture
Sources/Core/OriginValidator.swift- Pure validation logic in ApfelCore (unit-testable, no Hummingbird dependency)Sources/SecurityMiddleware.swift- HummingbirdRouterMiddlewarehandling origin check, token auth, and CORS- Middleware runs before all routes - catches every endpoint including future ones
Examples
# Default - safe, works like before
apfel --serve
# Local web app on port 3000
apfel --serve --cors --allowed-origins "http://localhost:3000"
# Token auth for shared machines
apfel --serve --token-auto
# Old behavior (wide open)
apfel --serve --footgunTests
- 99 unit tests (32 new for OriginValidator) - all pass
- 78 integration tests (18 new for security) - all pass
- Security tests include spawned server scenarios for
--token,--token-auto,--allowed-origins, and CORS interaction
Documentation
- Server Security Guide - detailed docs for all security flags, matching matrix, common scenarios, and prior art
Prior art
- Ollama CSRF - same port 11434,
fetch()exploit, no auth - Jupyter token auth - added after CSRF CVEs
- 0.0.0.0 Day - browsers block
0.0.0.0but Origin-based CSRF still works on127.0.0.1
Credit
This issue was originally reported via Hacker News discussion: https://news.ycombinator.com/item?id=47625563
Full Changelog: v0.6.14...v0.6.23