From 9aaacd11cc50666c669e90afb9b656f4bc42bdda Mon Sep 17 00:00:00 2001 From: Dmitrii Iurco Date: Mon, 27 Apr 2026 12:18:02 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20CSRF=20regression=20=E2=80=94=20grace=20?= =?UTF-8?q?period=20for=20old=20sessions,=20GET=20check-port/refresh-ip,?= =?UTF-8?q?=20Peers.jsx=20native=20fetch=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - check_csrf() now issues a token for sessions that predate CSRF (existing logins) instead of blocking them - /api/wireguard/check-port and /api/wireguard/refresh-ip accept GET so native fetch calls bypass the token requirement - WireGuard.jsx: changed three native fetch POST → GET for the above endpoints - Peers.jsx: add X-CSRF-Token header to three native fetch mutation calls (calendar collection, peer PUT, clear-reinstall) - api.js: export getCsrfToken() so non-Axios callers can read the current token Co-Authored-By: Claude Sonnet 4.6 --- api/app.py | 12 +++++++++--- webui/src/pages/Peers.jsx | 16 ++++++++++++---- webui/src/pages/WireGuard.jsx | 6 +++--- webui/src/services/api.js | 4 ++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/api/app.py b/api/app.py index 5285b01..656b7d5 100644 --- a/api/app.py +++ b/api/app.py @@ -261,8 +261,14 @@ def check_csrf(): path = request.path if not path.startswith('/api/') or path.startswith('/api/auth/'): return None - token_header = request.headers.get('X-CSRF-Token') token_session = session.get('csrf_token') + if not token_session: + # Session predates CSRF tokens (existing login) — issue a token now so + # the next request can carry it. Don't block this request; the client + # couldn't have known the token yet. + session['csrf_token'] = secrets.token_hex(32) + return None + token_header = request.headers.get('X-CSRF-Token') if not token_header or token_header != token_session: return jsonify({'error': 'CSRF token missing or invalid'}), 403 return None @@ -1762,7 +1768,7 @@ def get_server_config(): logger.error(f"Error getting server config: {e}") return jsonify({"error": str(e)}), 500 -@app.route('/api/wireguard/refresh-ip', methods=['POST']) +@app.route('/api/wireguard/refresh-ip', methods=['GET', 'POST']) def refresh_external_ip(): try: ip = wireguard_manager.get_external_ip(force_refresh=True) @@ -1788,7 +1794,7 @@ def apply_wireguard_enforcement(): except Exception as e: return jsonify({'error': str(e)}), 500 -@app.route('/api/wireguard/check-port', methods=['POST']) +@app.route('/api/wireguard/check-port', methods=['GET', 'POST']) def check_wireguard_port(): try: port_open = wireguard_manager.check_port_open() diff --git a/webui/src/pages/Peers.jsx b/webui/src/pages/Peers.jsx index 518ba4f..0c79e59 100644 --- a/webui/src/pages/Peers.jsx +++ b/webui/src/pages/Peers.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { Plus, Trash2, Edit, Eye, Shield, Copy, Download, Key, AlertTriangle, CheckCircle, Globe, Lock, Users, Server } from 'lucide-react'; -import { peerRegistryAPI, wireguardAPI } from '../services/api'; +import { peerRegistryAPI, wireguardAPI, getCsrfToken } from '../services/api'; import { useConfig } from '../contexts/ConfigContext'; import QRCode from 'qrcode'; @@ -194,7 +194,11 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; if (formData.create_calendar) { try { - await fetch(`/api/calendar/create-user-collection?user=${formData.name}`, { method: 'POST', credentials: 'include' }); + await fetch(`/api/calendar/create-user-collection?user=${formData.name}`, { + method: 'POST', + credentials: 'include', + headers: { 'X-CSRF-Token': getCsrfToken() || '' }, + }); } catch {} } @@ -225,7 +229,7 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; const r = await fetch(`/api/peers/${selectedPeer.name}`, { method: 'PUT', credentials: 'include', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() || '' }, body: JSON.stringify({ description: formData.description, internet_access: formData.internet_access, @@ -292,7 +296,11 @@ PersistentKeepalive = ${peer.persistent_keepalive || 25}`; const handleConfigDownloaded = async (peerName) => { try { - await fetch(`/api/peers/${peerName}/clear-reinstall`, { method: 'POST', credentials: 'include' }); + await fetch(`/api/peers/${peerName}/clear-reinstall`, { + method: 'POST', + credentials: 'include', + headers: { 'X-CSRF-Token': getCsrfToken() || '' }, + }); setPeers(ps => ps.map(p => p.name === peerName ? { ...p, config_needs_reinstall: false } : p)); } catch {} }; diff --git a/webui/src/pages/WireGuard.jsx b/webui/src/pages/WireGuard.jsx index 98a79c3..f0ed397 100644 --- a/webui/src/pages/WireGuard.jsx +++ b/webui/src/pages/WireGuard.jsx @@ -29,11 +29,11 @@ function WireGuard() { setIsRefreshingIp(true); try { // Refresh IP first (fast) - const ipResp = await fetch('/api/wireguard/refresh-ip', { method: 'POST', credentials: 'include' }); + const ipResp = await fetch('/api/wireguard/refresh-ip', { credentials: 'include' }); const ipData = await ipResp.json(); setServerConfig(prev => ({ ...prev, ...ipData, port_open: 'checking' })); // Then check port (slow — external call) - const portResp = await fetch('/api/wireguard/check-port', { method: 'POST', credentials: 'include' }); + const portResp = await fetch('/api/wireguard/check-port', { credentials: 'include' }); const portData = await portResp.json(); setServerConfig(prev => ({ ...prev, port_open: portData.port_open })); } catch (e) { @@ -56,7 +56,7 @@ function WireGuard() { if (serverConfigResponse) { setServerConfig({ ...serverConfigResponse, port_open: 'checking' }); // Check port asynchronously so page loads fast - fetch('/api/wireguard/check-port', { method: 'POST', credentials: 'include' }) + fetch('/api/wireguard/check-port', { credentials: 'include' }) .then(r => r.json()) .then(d => setServerConfig(prev => ({ ...prev, port_open: d.port_open ?? false }))) .catch(() => setServerConfig(prev => ({ ...prev, port_open: false }))); diff --git a/webui/src/services/api.js b/webui/src/services/api.js index 1a3ad20..66f6dc9 100644 --- a/webui/src/services/api.js +++ b/webui/src/services/api.js @@ -11,6 +11,10 @@ export function setCsrfToken(token) { _csrfToken = token; } +export function getCsrfToken() { + return _csrfToken; +} + // Create axios instance with base configuration const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || '',