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 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 ( 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)",