From 2bfc36baa0dc9d7ea36b3d2e97902a762d180040 Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 11 Mar 2026 00:53:51 +0000 Subject: [PATCH] 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 --- core/archipelago/src/api/handler.rs | 49 ++++++++++++++++++++--------- loop/plan.md | 2 +- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/core/archipelago/src/api/handler.rs b/core/archipelago/src/api/handler.rs index bdebe545..fa3526cc 100644 --- a/core/archipelago/src/api/handler.rs +++ b/core/archipelago/src/api/handler.rs @@ -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 { + 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 { + 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() diff --git a/loop/plan.md b/loop/plan.md index 2ce5bba3..32a69664 100644 --- a/loop/plan.md +++ b/loop/plan.md @@ -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.