From 87def09645875f9a3a0c6c45163d1923d8d3afea Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 25 Sep 2026 04:56:11 -0400 Subject: [PATCH 1/3] [FIX] qemu cache: git mirror clones only over http and https MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scheme of a git negotiation came from the request line the client writes, so "ssh://any-host/x.git/info/refs" made the cache run git clone over ssh, with its service account keys, toward a host the client chose. A git client reaches an HTTP proxy only over HTTP(S), so nothing cached is lost. DepotDeURL now refuses other schemes, the clone ends its options with "--", and GIT_ALLOW_PROTOCOL=http:https bounds git itself. Checked: ssh, git, file and ext refused, a real clone through the mirror still passes, full qemu_cache Go suite green. --- FR --- [FIX] qemu cache : le miroir git ne clone qu'en http et https Le schéma d'une négociation git venait de la ligne de requête écrite par le client : « ssh://hôte-quelconque/x.git/info/refs » faisait lancer au cache un git clone en ssh, avec les clés de son compte de service, vers un hôte choisi par le client. Un client git ne joint un mandataire HTTP qu'en HTTP(S) : rien de ce qui se met en cache n'est perdu. DepotDeURL refuse les autres schémas, le clone clôt ses options par « -- », et GIT_ALLOW_PROTOCOL=http:https borne git lui-même. Vérifié : ssh, git, file et ext refusés, un vrai clone à travers le miroir passe toujours, suite Go de qemu_cache au vert. Assisted-by: Claude Opus 5.5 --- script/qemu_cache/gitmirror.go | 17 ++++++++-- script/qemu_cache/gitmirror_test.go | 51 +++++++++++++++++++++++++++-- script/qemu_cache/main.go | 2 +- 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/script/qemu_cache/gitmirror.go b/script/qemu_cache/gitmirror.go index 96a4f4f..571b3f4 100644 --- a/script/qemu_cache/gitmirror.go +++ b/script/qemu_cache/gitmirror.go @@ -138,8 +138,14 @@ func (g *GitMirror) backend() string { // « https://h/o/d.git/info/refs?service=… » rend « https://h/o/d.git » et // « /info/refs ». Rend faux quand l'URL n'est pas une négociation : le // découpage n'aurait alors aucun sens. +// +// Rend faux aussi hors de http et https. Le schéma vient de la ligne de +// requête, que le client écrit, et un client git ne passe par un mandataire +// HTTP qu'en HTTP(S) : ssh et git:// ouvrent leur propre connexion. Accepter +// « ssh://hôte/… » ferait cloner le cache vers un hôte choisi par le client, +// avec les clés de son compte de service. func DepotDeURL(u *url.URL) (string, string, bool) { - if u == nil { + if u == nil || (u.Scheme != "http" && u.Scheme != "https") { return "", "", false } for _, s := range gitSmartPaths { @@ -226,7 +232,10 @@ func (g *GitMirror) Assurer(ctx context.Context, depot string) (string, bool) { if err := os.MkdirAll(filepath.Dir(chemin), 0o755); err != nil { return "", false } - if err := g.git(ctx, "", "clone", "--mirror", depot, chemin); err != nil { + // « -- » : le dépôt ne peut plus se lire comme une option de git. + if err := g.git( + ctx, "", "clone", "--mirror", "--", depot, chemin, + ); err != nil { // Un clonage à moitié fait laisserait un répertoire que la // prochaine requête prendrait pour un miroir valide. os.RemoveAll(chemin) @@ -414,10 +423,14 @@ func (g *GitMirror) gitBorne( // Aucune invite : un dépôt privé doit ÉCHOUER et retomber sur le relais, // et non bloquer le service en attendant un mot de passe que personne ne // tapera jamais. + // GIT_ALLOW_PROTOCOL borne les transports de git lui-même, clone comme + // remote update : le miroir ne sert que des négociations HTTP(S), et aucun + // dépôt ne doit l'emmener vers ssh, git:// ou un chemin local. cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_ASKPASS=/bin/true", "GCM_INTERACTIVE=never", + "GIT_ALLOW_PROTOCOL=http:https", ) sortie, err := cmd.CombinedOutput() if err != nil { diff --git a/script/qemu_cache/gitmirror_test.go b/script/qemu_cache/gitmirror_test.go index 53d274f..50e6556 100644 --- a/script/qemu_cache/gitmirror_test.go +++ b/script/qemu_cache/gitmirror_test.go @@ -51,6 +51,49 @@ func TestDepotDeURLRefuse(t *testing.T) { } } +// Le schéma vient de la ligne de requête absolue, que le client écrit. Un +// schéma autre que HTTP(S) ferait lancer « git clone » sur un transport que +// jamais un client passant par un mandataire HTTP n'emprunte — ssh, avec les +// clés du compte du service, vers un hôte choisi par le client. +func TestDepotDeURLRefuseLesSchemasNonHTTP(t *testing.T) { + for _, brut := range []string{ + "ssh://h/o/d.git/info/refs?service=git-upload-pack", + "git://h/o/d.git/info/refs?service=git-upload-pack", + "file:///srv/o/d.git/info/refs?service=git-upload-pack", + "ext::sh%20-c%20id/info/refs", + } { + u, err := url.Parse(brut) + if err != nil { + continue + } + if depot, _, ok := DepotDeURL(u); ok { + t.Errorf("%s accepté comme dépôt %q", brut, depot) + } + } + // url.Parse ramène le schéma en minuscules : la casse ne refuse rien. + u, _ := url.Parse("HTTP://h/o/d.git/info/refs?service=git-upload-pack") + if _, _, ok := DepotDeURL(u); !ok { + t.Error("HTTP en majuscules refusé") + } +} + +// Le dernier rempart est git lui-même : même une URL qui passerait le tri de +// DepotDeURL n'emprunte aucun transport hors HTTP(S). Le dépôt local, que git +// clone sans réseau ni sonde, prouve le refus sans dépendre d'aucun hôte. +func TestGitNEmprunteQueHTTP(t *testing.T) { + nu := depotDEssai(t) + g := &GitMirror{Dir: t.TempDir()} + err := g.git(context.Background(), "", "ls-remote", "--", "file://"+nu) + if err == nil { + t.Fatal("ls-remote file:// accepté") + } + // Le message suit la langue de git ; le transport, lui, y est nommé tel + // quel dans toutes les langues. + if !strings.Contains(err.Error(), "'file'") { + t.Errorf("refus inattendu : %v", err) + } +} + // Le chemin du miroir porte l'HÔTE : deux forges peuvent servir « /odoo/odoo », // et les confondre donnerait à l'une le contenu de l'autre. func TestCheminMiroirSepareLesForges(t *testing.T) { @@ -212,7 +255,9 @@ func TestClonerAuTraversDuMiroir(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery} + // L'URL que absoluteURL rend au proxy : schéma et hôte compris. + u := &url.URL{Scheme: "http", Host: r.Host, + Path: r.URL.Path, RawQuery: r.URL.RawQuery} _, reste, ok := DepotDeURL(u) if !ok { http.NotFound(w, r) @@ -413,7 +458,9 @@ func TestCeQueLeMiroirSertEstCompte(t *testing.T) { var pese int64 srv := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { - u := &url.URL{Path: r.URL.Path, RawQuery: r.URL.RawQuery} + // L'URL que absoluteURL rend au proxy : schéma et hôte compris. + u := &url.URL{Scheme: "http", Host: r.Host, + Path: r.URL.Path, RawQuery: r.URL.RawQuery} _, reste, ok := DepotDeURL(u) if !ok { http.NotFound(w, r) diff --git a/script/qemu_cache/main.go b/script/qemu_cache/main.go index 296ece5..a245eb4 100644 --- a/script/qemu_cache/main.go +++ b/script/qemu_cache/main.go @@ -25,7 +25,7 @@ import ( "time" ) -const version = "0.2.16" +const version = "0.2.17" func main() { var ( From b0c26caa6afefb340da7adc9ed8f49b44151ebc0 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 25 Sep 2026 05:15:20 -0400 Subject: [PATCH 2/3] [IMP] todo: icons on the TUI form and classic questions choices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interface chooser of the QEMU deployment and of the Odoo migration, and the matching preferences in Configuration, read as two plain lines. The TUI form now carries a clipboard icon and the line by line questions a speech balloon, in both languages. Both are single code point emoji, without the variation selector that shifts alignment on some terminals. Checked: both labels render with their icon in French and English, and test_todo_i18n passes. --- FR --- [IMP] todo : icônes sur les choix formulaire TUI et questions classiques Le choix d'interface du déploiement QEMU et de la migration Odoo, et les préférences correspondantes de Configuration, se lisaient comme deux lignes nues. Le formulaire TUI porte désormais un presse-papiers et les questions ligne par ligne une bulle, dans les deux langues. Ce sont des emoji d'un seul point de code, sans le sélecteur de variante qui décale l'alignement sur certains terminaux. Vérifié : les deux libellés s'affichent avec leur icône en français et en anglais, et test_todo_i18n passe. Assisted-by: Claude Opus 5.5 --- script/todo/todo_i18n.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 886ebdc..4a0d5a8 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -2480,12 +2480,12 @@ TRANSLATIONS = { "en": "Ask every time", }, "TUI form": { - "fr": "Formulaire TUI", - "en": "TUI form", + "fr": "📋 Formulaire TUI", + "en": "📋 TUI form", }, "Classic questions (line by line)": { - "fr": "Questions classiques (ligne par ligne)", - "en": "Classic questions (line by line)", + "fr": "💬 Questions classiques (ligne par ligne)", + "en": "💬 Classic questions (line by line)", }, "CLI output (easy to copy)": { "fr": "Sortie CLI (facile à copier)", From 1998625150934d02173d421a64c6b2b77c21c6c1 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Fri, 25 Sep 2026 05:39:19 -0400 Subject: [PATCH 3/3] [UPD] changelog: qemu cache git mirror limited to http(s), UI icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the branch for the next release: under Security, the git mirror of the QEMU cache that no longer follows an ssh:// or git:// repository named by a client, with the binary version 0.2.17 that tells a fixed cache apart; under Changed, the icons on the interface chooser. Checked: make doc_markdown regenerates both files, each language only in its own. --- FR --- [UPD] changelog : miroir git du cache qemu limité à http(s), icônes Consigne la branche pour la prochaine version : sous Sécurité, le miroir git du cache QEMU qui ne suit plus un dépôt ssh:// ou git:// nommé par un client, avec la version 0.2.17 du binaire qui distingue un cache corrigé ; sous Modifié, les icônes du choix d'interface. Vérifié : make doc_markdown régénère les deux fichiers, chaque langue dans le sien seulement. Assisted-by: Claude Opus 5.5 --- CHANGELOG.base.md | 4 ++++ CHANGELOG.fr.md | 2 ++ CHANGELOG.md | 2 ++ 3 files changed, 8 insertions(+) diff --git a/CHANGELOG.base.md b/CHANGELOG.base.md index 7debfaf..5fc683f 100644 --- a/CHANGELOG.base.md +++ b/CHANGELOG.base.md @@ -178,6 +178,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own - `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them - factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x +- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬 @@ -212,6 +213,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls - `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux - factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée +- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬 ## Fixed @@ -377,6 +379,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host - Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651 - Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081 +- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17 @@ -384,6 +387,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte - Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651 - Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081 +- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17 diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index aca6f7d..0934fc3 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -98,6 +98,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Dependabot réunit `aiobotocore`, `botocore` et `boto3` dans une seule demande de fusion, chaque aiobotocore n'acceptant qu'une plage étroite de botocore ; les correctifs de sécurité arrivent toujours seuls - `TODO › Transform data` lit Excel avec openpyxl 3.1.5 et xlsxwriter 3.2.9 ; le test de fuite qui garde l'épingle exacte d'openpyxl passe sur eux - factur-x exige 6.8 hors s390x, la version déjà verrouillée : une régénération ne peut plus retomber sur une 4.x ou 5.x non testée +- Le choix d'interface du déploiement QEMU et de la migration Odoo, et ses préférences dans `TODO › Configuration`, marquent le formulaire TUI d'un 📋 et les questions ligne par ligne d'un 💬 ## Corrigé @@ -178,6 +179,7 @@ au [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - En suivant une redirection, le cache ne transmet plus les identifiants du client (Authorization, Cookie, Proxy-Authorization) à un autre hôte - Odoo 18 installe `idna` 3.20 au lieu de la 3.6 qu'épinglent ses propres requirements, touchée par CVE-2024-3651 - Odoo 18 installe `requests` 2.32.4 au lieu du 2.31.0 qu'épinglent ses propres requirements, touché par CVE-2024-35195 et CVE-2024-47081 +- Le miroir git du cache QEMU ne clone plus qu'en `http` et `https` : un client pouvait nommer un dépôt `ssh://` ou `git://` dans sa requête et faire se connecter le cache, avec les clés de son compte de service, à un hôte de son choix. Les dépôts servis en HTTPS sont mis en miroir comme avant ; le binaire annonce 0.2.17 ## [1.8.0] - 2026-09-04 diff --git a/CHANGELOG.md b/CHANGELOG.md index 486a2ad..77f19f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Dependabot groups `aiobotocore`, `botocore` and `boto3` into one pull request, since each aiobotocore accepts only a narrow botocore range; security fixes still arrive on their own - `TODO › Transform data` reads Excel with openpyxl 3.1.5 and xlsxwriter 3.2.9; the leak test that guards openpyxl's exact pin passes on them - factur-x requires 6.8 outside s390x, the version already locked, so a regeneration can no longer fall back to an untested 4.x or 5.x +- The interface chooser of the QEMU deployment and of the Odoo migration, and its preferences in `TODO › Configuration`, mark the TUI form with 📋 and the line by line questions with 💬 ## Fixed @@ -178,6 +179,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Following a redirect, the cache no longer forwards the client's credentials (Authorization, Cookie, Proxy-Authorization) to another host - Odoo 18 installs `idna` 3.20 instead of the 3.6 its own requirements pin, which is affected by CVE-2024-3651 - Odoo 18 installs `requests` 2.32.4 instead of the 2.31.0 its own requirements pin, which is affected by CVE-2024-35195 and CVE-2024-47081 +- The git mirror of the QEMU cache clones only over `http` and `https`: a client could name an `ssh://` or `git://` repository in its request and make the cache connect, with its service account keys, to a host of its choosing. Repositories fetched over HTTPS are mirrored as before; the binary reports 0.2.17 ## [1.8.0] - 2026-09-04