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()

View File

@ -58,7 +58,7 @@
- [x] **BACK-05** — Implement CSRF protection on RPC layer. Address the High-severity finding from `docs/security-audit-2026-03-05.md`. Add CSRF token generation on login (return as cookie + response field), validate on all state-changing RPC calls. In `core/archipelago/src/api/rpc/mod.rs`, add `X-CSRF-Token` header check for non-GET methods. In `neode-ui/src/api/rpc-client.ts`, read the CSRF cookie and send it as header. **Acceptance**: RPC calls without CSRF token return 403; calls with correct token succeed.
- [ ] **BACK-06** — Fix CORS policy: restrict to same-origin. Address the High-severity CORS finding. In `core/archipelago/src/server.rs`, change `Access-Control-Allow-Origin: *` to same-origin only (no CORS header for same-origin requests, or explicit origin matching for allowed origins). **Acceptance**: Cross-origin requests from unknown origins are rejected.
- [x] **BACK-06** — Fix CORS policy: restrict to same-origin. Address the High-severity CORS finding. In `core/archipelago/src/server.rs`, change `Access-Control-Allow-Origin: *` to same-origin only (no CORS header for same-origin requests, or explicit origin matching for allowed origins). **Acceptance**: Cross-origin requests from unknown origins are rejected.
- [ ] **BACK-07** — Add Nginx security headers. In `image-recipe/configs/nginx-archipelago.conf`, add: `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`, `Content-Security-Policy` with appropriate directives, `Referrer-Policy: strict-origin-when-cross-origin`. Sync to server. **Acceptance**: `curl -I http://192.168.1.228` shows all security headers.