fix: CSRF 403 blocking all operations + reboot after install
Some checks failed
Container Orchestration Tests / unit-tests (push) Has been cancelled
Container Orchestration Tests / smoke-tests (push) Has been cancelled
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled

CSRF fix (THE BLOCKER):
- After remember-me session restore, the browser has a stale CSRF
  cookie but a new session token. ALL subsequent RPC calls return 403.
- Fix: exempt read-only polling methods (node-messages-received,
  server.echo, system.stats, tor.status, etc.) from CSRF validation.
  CSRF still protects state-changing operations (install, uninstall,
  start, stop, restart, settings changes).

Reboot fix:
- The separate /tmp/archipelago-reboot.sh approach failed because
  /bin/bash is on the squashfs which gets unmounted when USB is pulled.
- Fix: do everything inline in the installer script — show message,
  unmount USB, wait for Enter, then reboot. Use sysrq-trigger first
  (kernel-level, doesn't need userspace binaries).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-29 22:42:09 +01:00
parent 37f32f4e54
commit 89a9f69a9b
2 changed files with 47 additions and 30 deletions

View File

@@ -217,8 +217,14 @@ impl RpcHandler {
}
// CSRF protection: validate X-CSRF-Token header via HMAC derivation from session token.
// Skip CSRF check if session was just auto-restored from remember-me.
if !is_unauthenticated && new_session_cookies.is_none() {
// Skip CSRF for read-only methods (polling, status) — CSRF prevents state-changing forgery.
// Skip when session was just auto-restored from remember-me (browser has stale CSRF cookie).
let csrf_exempt = matches!(rpc_req.method.as_str(),
"node-messages-received" | "server.echo" | "system.stats" | "tor.status"
| "tor.onion-addresses" | "federation.list-nodes" | "system.get-settings"
| "system.get-node-key" | "system.get-metrics" | "system.get-version"
);
if !is_unauthenticated && new_session_cookies.is_none() && !csrf_exempt {
let csrf_header = parts
.headers
.get("x-csrf-token")
@@ -245,12 +251,24 @@ impl RpcHandler {
};
if !csrf_valid {
tracing::warn!(
method = %rpc_req.method,
has_session = session_token.is_some(),
has_header = csrf_header.is_some(),
"403 CSRF validation failed — rejecting RPC call"
);
// Debug: log expected vs received for diagnosis
if let (Some(token), Some(header)) = (&session_token, &csrf_header) {
let expected = derive_csrf_token(token).await;
tracing::warn!(
method = %rpc_req.method,
session_prefix = %&token[..8.min(token.len())],
csrf_prefix = %&header[..8.min(header.len())],
expected_prefix = %&expected[..8.min(expected.len())],
"403 CSRF mismatch — session/csrf/expected prefixes shown"
);
} else {
tracing::warn!(
method = %rpc_req.method,
has_session = session_token.is_some(),
has_header = csrf_header.is_some(),
"403 CSRF validation failed — rejecting RPC call"
);
}
return Ok(self.error_response(403, "CSRF token missing or invalid", StatusCode::FORBIDDEN));
}
}

View File

@@ -2601,29 +2601,18 @@ echo ""
# Suppress kernel messages on console (SquashFS errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
# Copy reboot script to tmpfs so it survives USB removal
cat > /tmp/archipelago-reboot.sh <<'REBOOTSCRIPT'
#!/bin/bash
# This script runs from tmpfs — safe after USB removal
O=$'\033[38;5;208m'
OD=$'\033[38;5;130m'
N=$'\033[0m'
echo -e " ${O}>>> REMOVE THE USB DRIVE NOW <<<${N}"
echo ""
echo -e " ${OD}Press Enter to reboot (or wait 30 seconds)${N}"
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
# Show completion message, unmount USB, then reboot
# All done inline — no separate script needed (avoids /bin/bash dependency on squashfs)
echo ""
echo -e " ${OD}Rebooting...${N}"
sleep 1
reboot -f
REBOOTSCRIPT
chmod +x /tmp/archipelago-reboot.sh
p "${ORANGE}>>> REMOVE THE USB DRIVE NOW <<<${NC}"
echo ""
p "${ORANGE_DIM}Press Enter to reboot (or wait 30 seconds)${NC}"
# Lazy-unmount live filesystem BEFORE telling user to pull USB
# Suppress kernel messages (squashfs errors when USB is pulled)
echo 1 > /proc/sys/kernel/printk 2>/dev/null || true
# Lazy-unmount live filesystem
exec 2>/dev/null
umount -l /run/live/medium 2>/dev/null || true
umount -l /lib/live/mount/medium 2>/dev/null || true
@@ -2636,8 +2625,18 @@ if [ -n "$BOOT_DEV" ]; then
fi
exec 2>&1
# Hand off to tmpfs-based script — survives USB removal
exec /bin/bash /tmp/archipelago-reboot.sh
# Wait for Enter or timeout
read -t 30 -s 2>/dev/null || true
echo ""
p "${ORANGE_DIM}Rebooting...${NC}"
sleep 1
# Force reboot — multiple methods, first one that works wins
echo b > /proc/sysrq-trigger 2>/dev/null || \
/sbin/reboot -f 2>/dev/null || \
/usr/sbin/reboot -f 2>/dev/null || \
kill -9 1 2>/dev/null
INSTALLER_SCRIPT
# For unbundled builds, patch the completion message to reflect no pre-loaded apps