fix: implement 22 security pentest remediation fixes

Server-side session management with SHA-256 hashed tokens and HttpOnly
cookies. Auth middleware gating all RPC/WS/proxy routes with method
allowlist. Login rate limiting (5/60s per IP). CORS restricted to
config origin. Docker registry allowlist. App ID and path validation.
P2P message sanitization (HTML + log injection). Onion address and
known-peer validation. Nginx security headers (CSP, X-Frame-Options,
etc.) and AIUI proxy auth. Systemd hardening (non-root, NoNewPrivileges,
ProtectSystem).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-06 03:26:56 +00:00
co-authored by Claude Opus 4.6
parent 6623dbc4ab
commit 6656d2f1d9
12 changed files with 503 additions and 77 deletions
+118 -7
View File
@@ -10,10 +10,12 @@ use crate::auth::AuthManager;
use crate::config::Config;
use crate::container::DevContainerOrchestrator;
use crate::port_allocator::PortAllocator;
use crate::session::{self, LoginRateLimiter, SessionStore};
use crate::state::StateManager;
use anyhow::{Context, Result};
use hyper::{Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use std::sync::{Arc, Mutex};
use tracing::{debug, error};
@@ -39,16 +41,29 @@ struct RpcError {
/// Default dev password when no user is set up (matches mock-backend).
pub(crate) const DEV_DEFAULT_PASSWORD: &str = "password123";
/// Methods that do not require a valid session cookie.
const UNAUTHENTICATED_METHODS: &[&str] = &[
"auth.login",
"auth.isOnboardingComplete",
"health",
];
pub struct RpcHandler {
config: Config,
auth_manager: AuthManager,
orchestrator: Option<Arc<DevContainerOrchestrator>>,
state_manager: Arc<StateManager>,
port_allocator: Arc<Mutex<PortAllocator>>,
pub session_store: SessionStore,
login_rate_limiter: LoginRateLimiter,
}
impl RpcHandler {
pub async fn new(config: Config, state_manager: Arc<StateManager>) -> Result<Self> {
pub async fn new(
config: Config,
state_manager: Arc<StateManager>,
session_store: SessionStore,
) -> Result<Self> {
let auth_manager = AuthManager::new(config.data_dir.clone());
let orchestrator = if config.dev_mode {
Some(Arc::new(
@@ -65,6 +80,8 @@ impl RpcHandler {
orchestrator,
state_manager,
port_allocator,
session_store,
login_rate_limiter: LoginRateLimiter::new(),
})
}
@@ -72,8 +89,10 @@ impl RpcHandler {
&self,
req: Request<hyper::Body>,
) -> Result<Response<hyper::Body>> {
// Read request body
let (_, body) = req.into_parts();
// Extract session cookie before consuming the request
let (parts, body) = req.into_parts();
let session_token = session::extract_session_cookie(&parts.headers);
let body_bytes = hyper::body::to_bytes(body).await
.context("Failed to read body")?;
@@ -82,6 +101,55 @@ impl RpcHandler {
debug!("RPC method: {}", rpc_req.method);
// Enforce authentication for non-allowlisted methods
let is_unauthenticated = UNAUTHENTICATED_METHODS.contains(&rpc_req.method.as_str());
if !is_unauthenticated {
let authenticated = match &session_token {
Some(token) => self.session_store.validate(token).await,
None => false,
};
if !authenticated {
let rpc_resp = RpcResponse {
result: None,
error: Some(RpcError {
code: 401,
message: "Unauthorized".to_string(),
data: None,
}),
};
let resp_body = serde_json::to_vec(&rpc_resp)
.context("Failed to serialize response")?;
return Ok(Response::builder()
.status(StatusCode::UNAUTHORIZED)
.header("Content-Type", "application/json")
.body(hyper::Body::from(resp_body))
.unwrap());
}
}
// Rate limit login attempts
if rpc_req.method == "auth.login" {
let client_ip = extract_client_ip(&parts.headers);
if !self.login_rate_limiter.check(client_ip).await {
let rpc_resp = RpcResponse {
result: None,
error: Some(RpcError {
code: 429,
message: "Too many login attempts. Try again later.".to_string(),
data: None,
}),
};
let resp_body = serde_json::to_vec(&rpc_resp)
.context("Failed to serialize response")?;
return Ok(Response::builder()
.status(StatusCode::TOO_MANY_REQUESTS)
.header("Content-Type", "application/json")
.header("Retry-After", "60")
.body(hyper::Body::from(resp_body))
.unwrap());
}
}
// Route to handler
let result = match rpc_req.method.as_str() {
"echo" => self.handle_echo(rpc_req.params).await,
@@ -158,14 +226,46 @@ impl RpcHandler {
}
};
let body = serde_json::to_vec(&rpc_resp)
let resp_body = serde_json::to_vec(&rpc_resp)
.context("Failed to serialize response")?;
Ok(Response::builder()
let mut response = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(hyper::Body::from(body))
.unwrap())
.body(hyper::Body::from(resp_body))
.unwrap();
// Track failed login attempts for rate limiting
if rpc_req.method == "auth.login" && rpc_resp.error.is_some() {
let client_ip = extract_client_ip(&parts.headers);
self.login_rate_limiter.record_failure(client_ip).await;
}
// On successful login, create a session and set the cookie
if rpc_req.method == "auth.login" && rpc_resp.error.is_none() {
let token = self.session_store.create().await;
response.headers_mut().insert(
"Set-Cookie",
format!("session={}; HttpOnly; SameSite=Strict; Path=/", token)
.parse()
.unwrap(),
);
}
// On logout, invalidate session and expire the cookie
if rpc_req.method == "auth.logout" {
if let Some(token) = &session_token {
self.session_store.remove(token).await;
}
response.headers_mut().insert(
"Set-Cookie",
"session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0"
.parse()
.unwrap(),
);
}
Ok(response)
}
async fn handle_echo(&self, params: Option<serde_json::Value>) -> Result<serde_json::Value> {
@@ -177,3 +277,14 @@ impl RpcHandler {
Ok(serde_json::json!({ "message": "Hello from Archipelago!" }))
}
}
/// Extract the client IP from request headers (X-Real-IP or X-Forwarded-For).
fn extract_client_ip(headers: &hyper::HeaderMap) -> IpAddr {
headers
.get("x-real-ip")
.or_else(|| headers.get("x-forwarded-for"))
.and_then(|v| v.to_str().ok())
.and_then(|s| s.split(',').next())
.and_then(|s| s.trim().parse::<IpAddr>().ok())
.unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST))
}