mirror of
https://github.com/Such-Software/smirk-backend-core.git
synced 2026-10-19 16:28:41 +02:00
fix(operator): harden S3/S2/S4 per adversarial review
9 confirmed findings (review wbdf3c0sd), all fixed:
S4 (capabilities):
- REGRESSION: /capabilities reported grin_relay:false for a working relay (S4
wrongly coupled it to Grin chain serviceability). The relay is a non-custodial
mailbox — wallet broadcasts locally — so it is decoupled again. Removed the now-
dead grin_relay downgrade note.
S3 (bootstrap latch):
- A DELETE of the latch row escaped Locked without Tampered detection on a
CLI-bootstrapped-but-no-users instance (adoption keyed off `users` only). Boot
now adopts on has_any_users() OR has_any_admin_keys(), so a deleted latch
re-latches Locked instead of silently dropping to uninitialized.
- bootstrap_admin's single-bootstrap guard was a check-then-insert race (two
concurrent `setup` could seed two admins). It now takes the admin-keys mutation
advisory lock (shared with add/revoke/rotate) at the top of its tx.
S2 (admin posture + landing):
- Hard bind guard matched loopback by string prefix ("localhost".starts_with) —
"localhost.evil.com" slipped past. Now resolves via to_socket_addrs and requires
EVERY resolved addr to be loopback.
- host_allowed's IPv6 loopback check was an unanchored prefix ("[::1]" matched
"[::1].evil"); now anchored to exactly "[::1]" / "[::1]:port". +regression tests.
- Anti-clickjacking headers (X-Frame-Options/CSP) are now applied on the 403
Host-reject path too (computed on both branches, headers added once).
- GET /api/v1/server-info now returns a BARE 404 when landing is off (the
{error,code} envelope itself hinted the route exists).
- Public landing `chains` now uses the SAME serviceability predicate as
/capabilities (no over-advertising an unserviceable chain).
- Removed the dead/misleading PUBLIC_EXPOSE_FEATURES toggle (never read; the
feature tuple is intentionally never emitted anonymously).
Dismissed after verification: latch chains-by-flag (dup), title length bound
(operator-owned + escaped), prices:true with empty PRICES_ASSETS (documented).
140 lib + 140 integration green, no openapi drift, scans clean.
This commit is contained in:
+13
-12
@@ -89,8 +89,8 @@ fn host_allowed(host: &str, onion: Option<&str>) -> bool {
|
||||
return true;
|
||||
}
|
||||
let h = host.to_ascii_lowercase();
|
||||
// IPv6 loopback (with or without :port), e.g. "[::1]" / "[::1]:8081".
|
||||
if h.starts_with("[::1]") || h == "::1" {
|
||||
// IPv6 loopback, anchored: exactly "[::1]" or "[::1]:port" (not "[::1].evil").
|
||||
if h == "[::1]" || h.starts_with("[::1]:") || h == "::1" {
|
||||
return true;
|
||||
}
|
||||
let bare = h.split(':').next().unwrap_or(&h);
|
||||
@@ -119,10 +119,13 @@ pub async fn admin_plane_guard(
|
||||
.get(axum::http::header::HOST)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.unwrap_or("");
|
||||
if !host_allowed(host, state.config.admin.onion.as_deref()) {
|
||||
return (StatusCode::FORBIDDEN, "forbidden").into_response();
|
||||
}
|
||||
let mut resp = next.run(req).await;
|
||||
// Compute the response on both paths, then apply the security headers once so
|
||||
// the 403 Host-reject carries them too.
|
||||
let mut resp = if host_allowed(host, state.config.admin.onion.as_deref()) {
|
||||
next.run(req).await
|
||||
} else {
|
||||
(StatusCode::FORBIDDEN, "forbidden").into_response()
|
||||
};
|
||||
let h = resp.headers_mut();
|
||||
h.insert("x-frame-options", HeaderValue::from_static("DENY"));
|
||||
h.insert(
|
||||
@@ -719,12 +722,6 @@ pub async fn admin_features(
|
||||
"grin",
|
||||
"GRIN_OWNER_API_SECRET unset",
|
||||
);
|
||||
note(
|
||||
cfg.features.grin_relay,
|
||||
effective.features.grin_relay,
|
||||
"grin_relay",
|
||||
"Grin chain not serviceable",
|
||||
);
|
||||
note(
|
||||
cfg.features.nostr_identity,
|
||||
effective.features.nostr_identity,
|
||||
@@ -771,6 +768,10 @@ mod tests {
|
||||
// A foreign host (DNS-rebinding) is rejected.
|
||||
assert!(!host_allowed("evil.example", None));
|
||||
assert!(!host_allowed("attacker.test:8081", None));
|
||||
// Anchored: a host merely STARTING with a loopback form is rejected.
|
||||
assert!(!host_allowed("[::1].evil.com", None));
|
||||
assert!(!host_allowed("localhost.evil.com", None));
|
||||
assert!(!host_allowed("127.0.0.1.evil.com", None));
|
||||
// The configured onion is allowed; others still rejected.
|
||||
assert!(host_allowed("abc.onion", Some("abc.onion")));
|
||||
assert!(host_allowed("abc.onion:80", Some("abc.onion")));
|
||||
|
||||
Reference in New Issue
Block a user