chore(release): v0.7.18

Corrige le refus "chemin de configuration hors du dossier attendu" :
le service exigeait %ProgramData%\WireGuard\ alors que l'app écrit sa
config dans %APPDATA%\WGSecure\ (seul dossier inscriptible par un
utilisateur non-admin). Ce chemin n'étant pas recalculable depuis
LocalSystem, la validation repose désormais sur l'usurpation
d'identité du client du pipe : le fichier doit être ouvrable par
l'appelant lui-même, plus des contrôles structurels sur le nom et le
dossier.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 08:17:59 +02:00
co-authored by Claude Sonnet 5
parent f73908a358
commit e96f829f8b
6 changed files with 66 additions and 24 deletions
+8
View File
@@ -6,6 +6,14 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
---
## [0.7.18] — 2026-09-05
### Corrigé
- **« Chemin de configuration hors du dossier attendu » à l'installation du tunnel par le service.** Le service exigeait que le `.conf` se trouve dans `%ProgramData%\WireGuard\`, alors que l'application l'écrit sous Windows dans `%APPDATA%\WGSecure\` (`get_client_config_path`) — le seul endroit où un utilisateur non-admin peut écrire. Ce dossier ne peut pas être recalculé par le service, qui tourne en LocalSystem et verrait le `%APPDATA%` de SYSTEM : la validation repose désormais sur l'usurpation d'identité du client du pipe (`ImpersonateNamedPipeClient`), le fichier devant être ouvrable **par l'appelant lui-même** — un utilisateur ne peut donc jamais faire lire à SYSTEM un fichier auquel il n'a pas déjà accès. S'y ajoutent des contrôles structurels (chemin absolu, sans remontée de répertoire, nommé exactement `<interface>.conf` dans un dossier `WGSecure`).
- Les messages de refus indiquent maintenant ce qui a été reçu **et** ce qui était attendu, au lieu du seul chemin rejeté.
---
## [0.7.17] — 2026-09-05
### Corrigé
+1 -1
View File
@@ -4,7 +4,7 @@
## ──────────────────────────────────────────────
APP := wgsecure
VERSION := 0.7.17
VERSION := 0.7.18
VENV := .venv
PYTHON := $(VENV)/bin/python3
PIP := $(VENV)/bin/pip
+2 -2
View File
@@ -1,6 +1,6 @@
# 🛡️ WGSecure (WGS)
🚀 v0.7.17 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard
🚀 v0.7.18 · 🐍 · 🪟🐧 · 🔐 MFA TOTP · 🛡️ WireGuard
**WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard avec une surcouche MFA (authentification à deux facteurs TOTP).
@@ -210,4 +210,4 @@ WGSecure/
## 👤 Auteur
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
Version : ![v0.7.17](https://img.shields.io/badge/v0.7.17-septembre%202026-2980b9?style=flat-square) — Septembre 2026
Version : ![v0.7.18](https://img.shields.io/badge/v0.7.18-septembre%202026-2980b9?style=flat-square) — Septembre 2026
+1 -1
View File
@@ -1,3 +1,3 @@
__version__ = "0.7.17"
__version__ = "0.7.18"
APP_NAME = "WGSecure"
APP_SHORT = "WGS"
+53 -19
View File
@@ -36,7 +36,7 @@ import win32service
import win32serviceutil
from app.core.dns import _nrpt_add_split_dns_ps, _nrpt_remove_split_dns_ps
from app.utils.platform_utils import get_wg_config_dir, wg_exe, wireguard_exe
from app.utils.platform_utils import wg_exe, wireguard_exe
_PIPE_NAME = r"\\.\pipe\WGSecureHelper"
# SYSTEM + Administrateurs : contrôle total. Utilisateurs interactifs
@@ -107,19 +107,53 @@ def _validate_server(server) -> str:
return server
def _validate_config_path(config_path, name: str) -> str:
"""Le fichier doit résider exactement dans le dossier de configuration
WireGuard et s'appeler <name>.conf — jamais un chemin arbitraire. Le
service ne lit ni n'écrit ce fichier ; il vérifie seulement qu'il
pointe l'app (non privilégiée) est censée l'avoir déjà écrit
avant de demander à wireguard.exe de l'installer.
def _validate_config_path(config_path, name: str, handle) -> str:
"""Valide le .conf que l'appelant demande d'installer.
Le chemin ne peut pas être comparé à un dossier fixe : l'app écrit sa
config dans le `%APPDATA%` de *l'utilisateur* (`get_client_config_path`
dans app/core/wireguard.py), or ce service tourne en LocalSystem son
propre `%APPDATA%` est celui de SYSTEM, pas celui de l'appelant, et il
n'a aucun moyen fiable de recalculer celui-ci.
Le contrôle réel est donc l'usurpation d'identité du client du pipe :
le fichier doit être ouvrable *par l'appelant lui-même*. Un utilisateur
ne peut ainsi jamais faire lire à SYSTEM un fichier auquel il n'a pas
déjà accès. S'y ajoutent des contrôles structurels bon marché : chemin
absolu, sans remontée de répertoire, nommé exactement `<name>.conf`
dans un dossier `WGSecure`.
"""
if not isinstance(config_path, str):
if not isinstance(config_path, str) or not config_path:
raise ValidationError("Chemin de configuration invalide")
expected = os.path.join(get_wg_config_dir(), f"{name}.conf")
if os.path.normcase(os.path.abspath(config_path)) != os.path.normcase(os.path.abspath(expected)):
raise ValidationError(f"Chemin de configuration hors du dossier attendu : {config_path!r}")
return config_path
path = os.path.abspath(config_path)
if ".." in path.replace("/", "\\").split("\\"):
raise ValidationError(f"Chemin de configuration suspect : {config_path!r}")
if os.path.basename(path).lower() != f"{name}.conf".lower():
raise ValidationError(
f"Le fichier doit s'appeler {name}.conf, reçu : {os.path.basename(path)!r}")
if os.path.basename(os.path.dirname(path)).lower() != "wgsecure":
raise ValidationError(
"Le fichier doit se trouver dans un dossier WGSecure, reçu : "
f"{os.path.dirname(path)!r}")
# Ouverture sous l'identité de l'appelant : échoue si le fichier ne lui
# appartient pas / ne lui est pas lisible. Fail closed — si
# l'usurpation elle-même échoue, on refuse.
try:
win32pipe.ImpersonateNamedPipeClient(handle)
except pywintypes.error as e:
raise ValidationError(f"Identité de l'appelant non vérifiable : {e}") from None
try:
with open(path, "rb"):
pass
except OSError as e:
raise ValidationError(
f"Fichier illisible pour l'appelant : {config_path!r} ({e})") from None
finally:
win32security.RevertToSelf()
return path
def _run(cmd: list[str], timeout: int = 30) -> tuple[int, str]:
@@ -136,20 +170,20 @@ def _run(cmd: list[str], timeout: int = 30) -> tuple[int, str]:
return -1, str(e)
def _handle_install_tunnel(req: dict) -> dict:
def _handle_install_tunnel(req: dict, handle) -> dict:
name = _validate_interface_name(req.get("name"))
config_path = _validate_config_path(req.get("config_path"), name)
config_path = _validate_config_path(req.get("config_path"), name, handle)
code, msg = _run([wireguard_exe(), "/installtunnelservice", config_path], timeout=90)
return {"ok": code == 0, "message": msg}
def _handle_uninstall_tunnel(req: dict) -> dict:
def _handle_uninstall_tunnel(req: dict, handle) -> dict:
name = _validate_interface_name(req.get("name"))
code, msg = _run([wireguard_exe(), "/uninstalltunnelservice", name], timeout=60)
return {"ok": code == 0, "message": msg}
def _handle_set_split_dns(req: dict) -> dict:
def _handle_set_split_dns(req: dict, handle) -> dict:
server = _validate_server(req.get("server"))
domains = _validate_domains(req.get("domains"))
rule_name = _validate_rule_name(req.get("rule_name"))
@@ -158,14 +192,14 @@ def _handle_set_split_dns(req: dict) -> dict:
return {"ok": code == 0, "message": msg}
def _handle_clear_split_dns(req: dict) -> dict:
def _handle_clear_split_dns(req: dict, handle) -> dict:
rule_name = _validate_rule_name(req.get("rule_name"))
script = _nrpt_remove_split_dns_ps(rule_name)
code, msg = _run(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], timeout=20)
return {"ok": code == 0, "message": msg}
def _handle_get_status(req: dict) -> dict:
def _handle_get_status(req: dict, handle) -> dict:
"""Lecture seule : "message" porte la sortie de `wg show`, pas une
erreur "ok" reflète juste si la commande a pu s'exécuter.
`latest_handshakes` : ajoute le sous-argument `latest-handshakes`
@@ -223,7 +257,7 @@ def _handle_connection(handle, logger: logging.Logger) -> None:
resp = {"ok": False, "message": f"Commande inconnue : {cmd!r}"}
else:
try:
resp = handler(req)
resp = handler(req, handle)
except ValidationError as e:
resp = {"ok": False, "message": str(e)}
logger.info("%s -> ok=%s", cmd, resp.get("ok"))
+1 -1
View File
@@ -5,7 +5,7 @@
; ──────────────────────────────────────────────
#define MyAppName "WGSecure"
#define MyAppVersion "0.7.17"
#define MyAppVersion "0.7.18"
#define MyAppPublisher "WGSecure"
#define MyAppExeName "wgsecure.exe"