fix: restrict CORS to same-origin with explicit origin validation

Replace blanket cors_origin() with validate_origin() that checks the
incoming Origin header against allowed origins (host IP + dev server).
Unknown origins no longer receive Access-Control-Allow-Origin headers.
Also added X-CSRF-Token to allowed CORS headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-11 00:53:51 +00:00
parent a7653d4c8b
commit 2bfc36baa0
2 changed files with 36 additions and 15 deletions

View File

@@ -55,9 +55,27 @@ impl ApiHandler {
.unwrap()
}
/// Derive the allowed CORS origin from the config host IP.
fn cors_origin(&self) -> String {
format!("http://{}", self.config.host_ip)
/// Allowed CORS origins derived from the config host IP.
fn allowed_origins(&self) -> Vec<String> {
vec![
format!("http://{}", self.config.host_ip),
format!("https://{}", self.config.host_ip),
"http://localhost:8100".to_string(), // Vite dev server
]
}
/// Validate the Origin header against allowed origins.
/// Returns the matched origin if valid, None if cross-origin is not allowed.
fn validate_origin(&self, headers: &hyper::HeaderMap) -> Option<String> {
let origin = headers
.get("origin")
.and_then(|v| v.to_str().ok())?;
let allowed = self.allowed_origins();
if allowed.iter().any(|a| a == origin) {
Some(origin.to_string())
} else {
None
}
}
pub async fn handle_request(
@@ -69,16 +87,17 @@ impl ApiHandler {
// Handle CORS preflight for all routes
if method == Method::OPTIONS {
let origin = self.cors_origin();
return Ok(Response::builder()
let mut builder = Response::builder()
.status(StatusCode::NO_CONTENT)
.header("Access-Control-Allow-Origin", &origin)
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "Content-Type")
.header("Access-Control-Allow-Credentials", "true")
.header("Vary", "Origin")
.body(hyper::Body::empty())
.unwrap());
.header("Vary", "Origin");
if let Some(origin) = self.validate_origin(req.headers()) {
builder = builder
.header("Access-Control-Allow-Origin", &origin)
.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
.header("Access-Control-Allow-Headers", "Content-Type, X-CSRF-Token")
.header("Access-Control-Allow-Credentials", "true");
}
return Ok(builder.body(hyper::Body::empty()).unwrap());
}
// WebSocket upgrade — validate session before upgrading
@@ -131,7 +150,8 @@ impl ApiHandler {
if !self.is_authenticated(&headers).await {
return Ok(Self::unauthorized());
}
Self::handle_container_logs_http(self.rpc_handler.clone(), path, &self.cors_origin()).await
let origin = self.validate_origin(&headers).unwrap_or_default();
Self::handle_container_logs_http(self.rpc_handler.clone(), path, &origin).await
}
// LND proxy — requires session
@@ -139,7 +159,8 @@ impl ApiHandler {
if !self.is_authenticated(&headers).await {
return Ok(Self::unauthorized());
}
Self::handle_lnd_proxy(path, &self.cors_origin()).await
let origin = self.validate_origin(&headers).unwrap_or_default();
Self::handle_lnd_proxy(path, &origin).await
}
_ => Ok(Response::builder()