feat(security): implement AES-256-GCM encryption for API keys and HMAC-signed session tokens

This commit introduces:
- AES-256-GCM encryption for LLM provider API keys in the database.
- HMAC-SHA256 signed session tokens with activity-based refresh logic.
- Standardized frontend XSS protection using a global escapeHtml utility.
- Hardened security headers and request body size limits.
- Improved database integrity with foreign key enforcement and atomic transactions.
- Integration tests for the full encrypted key storage and proxy usage lifecycle.
This commit is contained in:
2026-03-06 14:17:56 -05:00
parent 149a7c3a29
commit 9b8483e797
28 changed files with 1260 additions and 227 deletions
+54
View File
@@ -11,10 +11,18 @@ mod users;
mod websocket;
use axum::{
extract::{Request, State},
middleware::Next,
response::Response,
Router,
routing::{delete, get, post, put},
};
use axum::http::{header, HeaderValue};
use serde::Serialize;
use tower_http::{
limit::RequestBodyLimitLayer,
set_header::SetResponseHeaderLayer,
};
use crate::state::AppState;
use sessions::SessionManager;
@@ -52,6 +60,21 @@ impl<T> ApiResponse<T> {
}
}
/// Rate limiting middleware for dashboard routes that extracts AppState from DashboardState.
async fn dashboard_rate_limit_middleware(
State(dashboard_state): State<DashboardState>,
request: Request,
next: Next,
) -> Result<Response, crate::errors::AppError> {
// Delegate to the existing rate limit middleware with AppState
crate::rate_limiting::middleware::rate_limit_middleware(
State(dashboard_state.app_state),
request,
next,
)
.await
}
// Dashboard routes
pub fn router(state: AppState) -> Router {
let session_manager = SessionManager::new(24); // 24-hour session TTL
@@ -60,6 +83,26 @@ pub fn router(state: AppState) -> Router {
session_manager,
};
// Security headers
let csp_header: SetResponseHeaderLayer<HeaderValue> = SetResponseHeaderLayer::overriding(
header::CONTENT_SECURITY_POLICY,
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws:;"
.parse()
.unwrap(),
);
let x_frame_options: SetResponseHeaderLayer<HeaderValue> = SetResponseHeaderLayer::overriding(
header::X_FRAME_OPTIONS,
"DENY".parse().unwrap(),
);
let x_content_type_options: SetResponseHeaderLayer<HeaderValue> = SetResponseHeaderLayer::overriding(
header::X_CONTENT_TYPE_OPTIONS,
"nosniff".parse().unwrap(),
);
let strict_transport_security: SetResponseHeaderLayer<HeaderValue> = SetResponseHeaderLayer::overriding(
header::STRICT_TRANSPORT_SECURITY,
"max-age=31536000; includeSubDomains".parse().unwrap(),
);
Router::new()
// Static file serving
.fallback_service(tower_http::services::ServeDir::new("static"))
@@ -119,5 +162,16 @@ pub fn router(state: AppState) -> Router {
"/api/system/settings",
get(system::handle_get_settings).post(system::handle_update_settings),
)
// Security layers
.layer(RequestBodyLimitLayer::new(10 * 1024 * 1024)) // 10 MB limit
.layer(csp_header)
.layer(x_frame_options)
.layer(x_content_type_options)
.layer(strict_transport_security)
// Rate limiting middleware
.layer(axum::middleware::from_fn_with_state(
dashboard_state.clone(),
dashboard_rate_limit_middleware,
))
.with_state(dashboard_state)
}