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:
parent
a7653d4c8b
commit
2bfc36baa0
@ -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()
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user