Security
About 423 wordsAbout 1 min
Security in Spora
Credential Encryption
LLM API keys and other secrets are encrypted at rest using libsodium secretbox (XSalsa20 stream cipher + Poly1305 MAC), authenticated encryption with a 32-byte master key (SODIUM_CRYPTO_SECRETBOX_KEYBYTES). The implementation lives in app/Core/SecurityManager.php (encrypt() / decrypt()).
The master key is resolved, in order, from:
SPORA_SECRET_KEYenv var — must be base64-encoded 32 raw bytesSPORA_KEY_PATHenv var — path to a 32-byte binary key fileconfig['key_path'](auto-set tostorage/secret.keyon first run ofspora:installordb:seed)
See app/Core/ContainerDefinitions.php:237-267 for the resolution chain.
Protect this key — anyone with access can decrypt all stored credentials.
API Authentication
Session-based authentication via delight-im/auth (Delight\Auth\Auth). The session is PHP’s native session, started by Symfony’s Request; the cookie name follows PHP defaults and is not set explicitly by Spora.
State-changing requests (POST/PUT/PATCH/DELETE) must include a CSRF token in the X-CSRF-Token request header. The token is generated by Spora\Security\CsrfTokenService (app/Security/CsrfTokenService.php), stored in the csrf_token session key, and issued to the client as the data.csrf_token field of POST /api/v1/auth/login and GET /api/v1/auth/me responses. The middleware that enforces it is Spora\Http\Middleware\CsrfMiddleware (app/Http/Middleware/CsrfMiddleware.php:30).
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/auth/login | Authenticate (issues data.csrf_token) |
| POST | /api/v1/auth/logout | End session (requires X-CSRF-Token) |
| GET | /api/v1/auth/me | Current user (returns data.csrf_token) |
Additional auth endpoints (registration, password change, email verification, etc.) are listed in the API reference and registered in app/Core/routes.php:44-55.
CSRF-exempt endpoints — the following pre-session auth endpoints are exempt from the CSRF check, because the session is not yet established when they run: /auth/register, /auth/forgot-password, /auth/reset-password, /auth/email/confirm, /auth/verification/resend, /auth/verify/{selector}. /health and /api/v1/config are also unauthenticated.
Rate Limiting
Spora’s rate limiter is a simple in-memory IP-based sliding window (Spora\Services\RateLimiter, app/Services/RateLimiter.php). The limit is applied on all auth-related endpoints, not only login:
| Endpoint Type | Limit | Source |
|---|---|---|
Authentication (/api/v1/auth/login, /api/v1/auth/register) | 5 req / 60 s | AuthController::RATE_LIMIT_* (app/Http/AuthController.php:26-27) |
Password reset (/api/v1/auth/forgot-password) | 5 req / 60 s | same constants |
Verification resend (/api/v1/auth/verification/resend) | 5 req / 60 s | same constants |
Other endpoints are not currently rate-limited.
Plugin Risks
Plugins are not sandboxed — they run as ordinary PHP code with full access to the application, the database, the file system, and any decrypted credentials. The plugin trust model and lifecycle are documented in the Plugin system(Open in new window) page under the Security section.