feat(sync): http:// limite au reseau local, Basic des la 1re requete

APK : le clair est permis par Android et trie dans TSyncClient.RefusalReason
(IsLocalHost : RFC 1918, 127.x, .local, .lan, nom sans point) - chacun pointe
vers son propre WebDAV sans IP connue a la compilation ; vers Internet, HTTPS.
PC : Authorization Basic envoye d'emblee, plus de 401 anonyme ni de PUT du
coffre envoye deux fois.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-10-02 23:26:17 +01:00
parent 3051c3c392
commit 0604344c60
4 changed files with 79 additions and 32 deletions
+54 -4
View File
@@ -56,6 +56,11 @@ type
Error: string; // message brut, pour le journal
end;
/// Vrai pour une adresse du reseau local (RFC 1918, boucle, .local, .lan,
/// nom sans point) : le seul endroit ou le HTTP en clair est accepte.
function IsLocalHost(const AHost: string): Boolean;
type
TSyncClient = class
private
FBaseUrl: string;
@@ -64,6 +69,7 @@ type
/// Attente de la POIGNEE DE MAIN, distincte de celle de la reponse.
FConnectMs: Integer;
FPrivacyChecked: Boolean;
function PublicRepoWarning: string;
public
constructor Create(const ABaseUrl, AToken: string);
@@ -82,11 +88,12 @@ type
/// l'ecriture (412 = un autre appareil a publie entre-temps).
function IsWebDav: Boolean;
/// Refus LOCAL, avant tout envoi : http:// hors reseau local, puis depot public.
/// UN DEPOT PUBLIC EST REFUSE (Gitea comme GitHub) : chaque synchro est un
/// commit, et l'historique garde tous les instantanes, attaquables hors
/// ligne par quiconque peut les lire. Message a afficher, ou '' si le depot
/// est prive ou si on n'a pas pu le savoir. Verifie une fois par instance.
function PublicRepoWarning: string;
function RefusalReason: string;
/// POURQUOI UN 404 OU UN 401, en clair pour l'utilisateur. Gitea repond 404
/// aussi bien pour un fichier absent que pour un depot ou un proprietaire
@@ -113,7 +120,7 @@ type
implementation
uses
System.JSON, System.NetEncoding;
System.JSON, System.NetEncoding, System.StrUtils;
function TSyncClient.IsGitea: Boolean;
begin
@@ -207,6 +214,49 @@ begin
if LP > 0 then Result := Copy(AContentsUrl, 1, LP - 1) else Result := '';
end;
/// HTTP EN CLAIR : RESEAU LOCAL SEULEMENT. Android le permet partout
/// (network_security_config) pour que chacun pointe vers SON serveur WebDAV,
/// a une adresse qu'on ne connait pas a la compilation ; le tri se fait donc
/// ici. Vers Internet, le jeton ne part qu'en HTTPS.
/// ponytail: un Wi-Fi etranger qui reutilise la meme plage recevrait le jeton
/// en clair - le coffre, lui, reste chiffre. HTTPS chez soi supprime le risque.
function IsLocalHost(const AHost: string): Boolean;
var
LParts: TArray<string>;
A, B: Integer;
begin
var LHost := AHost.ToLower;
if (LHost = 'localhost') or LHost.EndsWith('.local') or LHost.EndsWith('.lan') or
(LHost.IndexOf('.') < 0) and (LHost <> '') then
Exit(True);
LParts := LHost.Split(['.']);
if (Length(LParts) <> 4) or not TryStrToInt(LParts[0], A) or
not TryStrToInt(LParts[1], B) then
Exit(False);
for var LPart in LParts do
if not TryStrToInt(LPart, A) then Exit(False);
A := StrToInt(LParts[0]);
Result := (A = 10) or (A = 127) or ((A = 192) and (B = 168)) or
((A = 172) and (B >= 16) and (B <= 31));
end;
function TSyncClient.RefusalReason: string;
var
LHost: string;
begin
if StartsText('http://', FBaseUrl) then
begin
try
LHost := TURI.Create(FBaseUrl).Host;
except
LHost := '';
end;
if not IsLocalHost(LHost) then
Exit('http:// is only allowed on the local network (192.168.x.x, 10.x.x.x, .local, .lan) - use https:// for ' + LHost);
end;
Result := PublicRepoWarning;
end;
function TSyncClient.PublicRepoWarning: string;
var
LHttp: THTTPClient;
@@ -389,7 +439,7 @@ begin
Result.Error := 'Ecriture disponible seulement vers Gitea, GitHub ou WebDAV (jeton utilisateur:mot-de-passe)';
Exit;
end;
Result.Error := PublicRepoWarning;
Result.Error := RefusalReason;
if Result.Error <> '' then
begin
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
@@ -522,7 +572,7 @@ begin
Result.Error := 'URL ou jeton non configure';
Exit;
end;
Result.Error := PublicRepoWarning;
Result.Error := RefusalReason;
if Result.Error <> '' then
begin
Result.Status := ssRefused; // HttpCode 0 : refus local, pas du serveur
+9 -4
View File
@@ -434,10 +434,15 @@ l'ouverture, écriture 204, puis synchro du poste qui reprend la modification.
- **Une URL qui finit en `.json` n'est plus réécrite** par `normalizeRepoUrl`
(elle en faisait une URL d'API Gitea) — sauf les formes longues d'un dépôt,
`/src/branch/…` et `/blob/…`.
- **Clair autorisé vers `192.168.1.28` seulement** (IP fixe du poste), dans
`network_security_config.xml` : le mot de passe WebDAV passe en clair sur le
Wi-Fi, le coffre reste chiffré. À retirer quand le serveur parlera HTTPS.
Android ne sait pas autoriser « le réseau local » : chaque adresse se nomme.
- **`http://` : réseau local seulement**, pour que chacun (les amis compris)
pointe vers son propre serveur sans adresse connue à la compilation. Android
ne sait autoriser ni plage ni masque : `network_security_config.xml` permet
donc le clair partout, et `TSyncClient.RefusalReason` refuse **avant tout
envoi** un `http://` dont l'hôte n'est pas local (`IsLocalHost` : RFC 1918,
`127.x`, `.local`, `.lan`, nom sans point). Vers Internet, HTTPS obligatoire,
et le refus s'affiche dans les réglages. Risque assumé : un Wi-Fi étranger
qui réutilise la même plage recevrait le mot de passe WebDAV en clair — le
coffre, lui, reste chiffré.
- **Côté poste** : pare-feu Windows (règle `WebDAV 8080`, `LocalSubnet`) **et**
Portmaster (entrant autorisé pour Python) — sans le second, le port restait
fermé malgré la règle.
@@ -1,30 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Le trafic en clair n'est permis que vers la BOUCLE LOCALE, et interdit
partout ailleurs.
Le clair est PERMIS ICI, et trie dans le code (PMA.SyncClient.IsLocalHost).
Il l'etait auparavant partout, via usesCleartextTraffic="true". C'etait pour
une seule raison : l'essai local passe par "adb reverse", qui fait voir le
poste comme 127.0.0.1 et parle HTTP. Mais l'attribut ne distingue pas la
boucle locale du reste du monde - une URL mal saisie, ou un serveur repassant
en HTTP, envoyait le jeton en clair sur le reseau sans que rien ne le signale.
Ici la permission est nominative. Le jeton ne peut plus sortir qu'en TLS, et
une URL "http://" vers un vrai hote echoue tout de suite, ce qui est
exactement le comportement voulu : bruyant, pas silencieux.
cleartextTrafficPermitted="false" a la racine n'est pas redondant avec le
defaut d'Android 9+ : il vaut aussi pour les versions anterieures, et il dit
l'intention noir sur blanc.
Chacun pointe vers SON serveur WebDAV, souvent en http:// sur son reseau
local, a une adresse qu'on ne connait pas a la compilation - et Android ne
sait autoriser ni une plage ni un masque, seulement des noms un par un.
Avant tout envoi, la synchro refuse donc tout http:// qui ne vise pas le
reseau local (RFC 1918, boucle, .local, .lan, nom sans point) : vers
Internet, le jeton ne part qu'en HTTPS, et le refus s'affiche au lieu de
laisser passer en silence.
-->
<network-security-config>
<base-config cleartextTrafficPermitted="false" />
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
<domain includeSubdomains="false">localhost</domain>
<!-- ponytail: WebDAV local du poste (IP fixe), en clair sur le Wi-Fi.
Le coffre reste chiffre ; le mot de passe WebDAV, lui, passe en
clair. A retirer quand le serveur parlera HTTPS. -->
<domain includeSubdomains="false">192.168.1.28</domain>
</domain-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
+7
View File
@@ -1757,6 +1757,13 @@ begin
end
else if (LUser <> '') then
begin
// BASIC DES LA PREMIERE REQUETE, comme le telephone. Laisse a
// Windows, l'authentification partait d'abord en anonyme : 401,
// puis la meme requete - et un PUT envoyait le coffre deux fois.
// Les identifiants restent aussi en reserve pour un serveur Digest.
LHttp.CustomHeaders['Authorization'] := 'Basic ' +
TNetEncoding.Base64.Encode(LUser + ':' + LPwd)
.Replace(#13, '').Replace(#10, '');
LHttp.CredentialsStorage.AddCredential(
System.Net.URLClient.TCredentialsStorage.TCredential.Create(
System.Net.URLClient.TAuthTargetType.Server, '', '', LUser, LPwd));