How Unsafe Fall-Through in getWhereConditions Happens in Sequelize and How to Fix It
Introduction
The server/package-lock.json file in this application pins the version of Sequelize — one of the most widely used Node.js ORMs — and that single version number turned out to be the difference between a safe database layer and one vulnerable to SQL injection. Trivy's dependency scanner flagged sequelize@6.26.0 for CVE-2023-22579, a critical-severity vulnerability rooted in an unsafe fall-through inside Sequelize's internal getWhereConditions function.
What makes this vulnerability particularly insidious is that it doesn't require a developer to write obviously unsafe code. The flaw lives inside Sequelize itself: when building WHERE clauses, the library's query-construction logic would fall through to treating certain expressions — specifically those containing parentheses — as raw SQL attributes by default. That means user-controlled data that happened to include parentheses could slip past Sequelize's normal sanitization and land directly in a SQL query.
For developers building applications on top of Sequelize, this is a sobering reminder that ORM frameworks are not a silver bullet against SQL injection. The ORM itself can be the vulnerable component.
The Vulnerability Explained
What Is "Default Raw Attribute" Support?
Sequelize's query builder constructs SQL WHERE clauses by inspecting the attributes passed to methods like findAll, findOne, update, and destroy. Internally, getWhereConditions processes these attributes and decides how to serialize them into SQL.
The vulnerability — tracked as both CVE-2023-22579 and the related CVE-2023-22578 — stems from the fact that when getWhereConditions encountered an attribute value wrapped in parentheses, it would fall through to a code path that treated the value as a raw SQL fragment, without escaping or parameterizing it.
In simplified terms, the vulnerable behavior looked like this:
// Vulnerable Sequelize 6.26.0 behavior
// If a user-controlled value arrives as: "(1=1 OR 1=1)"
// getWhereConditions falls through and emits it verbatim into the SQL:
Model.findAll({
where: {
username: userInput // if userInput = "(admin' OR '1'='1')"
}
});
// Generated SQL (vulnerable):
// SELECT * FROM users WHERE username = (admin' OR '1'='1')
// The parenthesized expression bypasses normal quoting
The "fall-through" means there was a conditional check in getWhereConditions that, when it failed to match a known safe pattern, did not reject the input — it continued down to a code path that emitted the value as raw SQL. This is a classic unsafe default: rather than failing closed (rejecting unrecognized input), the function failed open (passing it through).
How Could This Be Exploited?
Consider an application endpoint that accepts a filter parameter from an HTTP request and passes it into a Sequelize query:
// Example vulnerable application code
app.get('/api/users', async (req, res) => {
const users = await User.findAll({
where: {
role: req.query.role // User-controlled input
}
});
res.json(users);
});
With Sequelize 6.26.0, an attacker could craft a request like:
GET /api/users?role=(admin' UNION SELECT password,username,null FROM users--)
Because the parenthesized expression triggers the unsafe fall-through in getWhereConditions, this value could be emitted into the SQL query as a raw fragment, potentially allowing:
- Data exfiltration via
UNION SELECTattacks - Authentication bypass via tautology injections (
OR 1=1) - Data destruction if the application has write permissions
The real-world impact depends on the database permissions granted to the application's database user, but in the worst case, an attacker with access to this endpoint could read every row in every table the application has access to.
The Vulnerable Dependency Version
The lock file pinned the vulnerable version explicitly:
// server/package-lock.json (BEFORE)
"node_modules/sequelize": {
"version": "6.26.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.26.0.tgz",
"integrity": "sha512-Xv82z1FdSn/qwB1IObSxIHV519cFk/vSD28vWs8Y0VucQLn7pK2x2jYjf2Qg/rBUQbCVprDdU7RPf+55rrkc0A=="
}
And the package.json range ^6.26.0 would not automatically resolve to a patched version without an explicit update, since the lock file overrides the semver range.
The Fix
The fix is a targeted dependency upgrade: Sequelize is bumped from 6.26.0 to 6.29.0. This version contains Sequelize's own internal patch to getWhereConditions that removes the unsafe default fall-through for raw attribute handling when parentheses are present.
Before and After: package-lock.json
// server/package-lock.json
"node_modules/sequelize": {
- "version": "6.26.0",
- "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.26.0.tgz",
- "integrity": "sha512-Xv82z1FdSn/qwB1IObSxIHV519cFk/..."
+ "version": "6.29.0",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.29.0.tgz",
+ "integrity": "sha512-m8Wi90rs3NZP9coXE52c7PL4Q078nwYZXqt1IxPvgki7nOFn0p/F0eKsYDBXCPw9G8/BCEa6zZNk0DQUAT4ypA==",
+ "license": "MIT"
}
Before and After: package.json
// server/package.json
-"sequelize": "^6.26.0",
+"sequelize": "^6.29.0",
Both files must be updated together. The package.json change updates the declared minimum version so future npm install runs won't resolve back to a vulnerable version. The package-lock.json change ensures the exact resolved version is pinned to the patched release immediately.
The retry-as-promised Transitive Dependency
The fix also updates a transitive dependency, retry-as-promised, from 6.1.0 to 7.1.1:
"node_modules/retry-as-promised": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
- "integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
+ "license": "MIT"
}
Sequelize 6.29.0 requires the updated retry-as-promised 7.x as part of its own dependency graph. This is a normal transitive update that comes along with the Sequelize upgrade and does not introduce any breaking changes for application code.
Why This Fix Works
In Sequelize 6.29.0, the internal getWhereConditions function was patched to not default to raw attribute mode when it encounters parenthesized expressions. Instead, the function now requires an explicit opt-in to raw SQL — developers must use Sequelize.literal() to pass raw SQL fragments, making the intent explicit and auditable. Implicit raw attribute support through parentheses is disabled, closing the fall-through path entirely.
Prevention & Best Practices
1. Keep ORM Dependencies Updated
ORMs like Sequelize are large, complex libraries with their own SQL-generation logic. Vulnerabilities in the ORM layer can affect every query in your application, regardless of how carefully individual queries are written. Subscribe to security advisories for your ORM and treat critical upgrades as urgent.
2. Use Explicit Raw SQL Sparingly
When you do need raw SQL in Sequelize, always use Sequelize.literal() explicitly, and never pass user-controlled data into it without parameterization:
// ❌ Dangerous — never do this
Model.findAll({
where: Sequelize.literal(`status = '${userInput}'`)
});
// ✅ Safe — use parameterized replacements
Model.findAll({
where: Sequelize.literal('status = :status'),
replacements: { status: userInput }
});
3. Use Sequelize Operators for Complex Conditions
Instead of constructing complex WHERE conditions with raw strings, use Sequelize's built-in Op operators:
const { Op } = require('sequelize');
// ✅ Safe — Sequelize handles escaping
Model.findAll({
where: {
age: { [Op.gt]: userInput },
name: { [Op.like]: `%${sanitizedInput}%` }
}
});
4. Scan Dependencies in CI/CD
The vulnerability was caught by Trivy scanning package-lock.json. Integrate dependency scanning into your CI/CD pipeline:
# Example: Trivy scan in CI
trivy fs --security-checks vuln ./server/package-lock.json
Tools to consider:
- Trivy — container and filesystem vulnerability scanner
- npm audit — built-in Node.js dependency auditing
- Snyk — developer-first security scanning
- OWASP Dependency-Check — open-source dependency auditing
5. Apply the Principle of Least Privilege to Database Users
Even if SQL injection occurs, limiting the database user's permissions reduces the blast radius. The application's database user should only have SELECT, INSERT, UPDATE, and DELETE on the specific tables it needs — never DROP, CREATE, or cross-database access.
Security Standards Reference
- OWASP Top 10: A03:2021 – Injection
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command
- OWASP SQL Injection Prevention Cheat Sheet: Use parameterized queries and prepared statements as the primary defense
Key Takeaways
- The
getWhereConditionsfall-through in Sequelize 6.26.0 meant that parenthesized user input could bypass ORM-level SQL sanitization entirely — this was not a developer mistake but a flaw in the library itself. - Pinning
sequelizeto^6.26.0inpackage.jsonwas not sufficient protection — the lock file held the exact vulnerable version and would not auto-update without an explicit intervention. - Upgrading both
package.jsonandpackage-lock.jsonis required — updating only one file leaves the other out of sync and may not actually change the installed version. - The transitive dependency
retry-as-promisedalso needed updating — Sequelize 6.29.0 requiresretry-as-promised@7.x, illustrating that security upgrades sometimes carry necessary transitive dependency changes. - ORM frameworks are not immune to SQL injection vulnerabilities — always treat ORM library updates with the same urgency as application code security patches.
How Orbis AppSec Detected This
- Source: The
sequelizepackage version6.26.0declared inserver/package-lock.json, which is consumed by any application code passing user-influenced data to Sequelize query methods (e.g.,findAll,findOne,update). - Sink: Sequelize's internal
getWhereConditionsfunction, which constructs SQLWHEREclauses from query attributes — the unsafe fall-through in this function allows parenthesized expressions to reach the SQL engine as raw fragments. - Missing control: Sequelize 6.26.0 lacked an explicit gate in
getWhereConditionsto reject or require opt-in for raw attribute expressions containing parentheses, defaulting to unsafe pass-through behavior instead of failing closed. - CWE: CWE-89 — Improper Neutralization of Special Elements used in an SQL Command (SQL Injection)
- Fix: Upgraded
sequelizefrom6.26.0to6.29.0in bothserver/package.jsonandserver/package-lock.json, removing the unsafe default raw attribute fall-through at the framework level.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2023-22579 is a stark reminder that SQL injection threats don't always originate from application code — they can live inside the very frameworks developers trust to protect them. The unsafe fall-through in Sequelize's getWhereConditions function turned a routine ORM query into a potential SQL injection vector, simply because the library defaulted to treating parenthesized expressions as raw SQL.
The fix is straightforward — upgrade Sequelize from 6.26.0 to 6.29.0 — but the lesson is broader: dependency security is application security. Regularly audit your package-lock.json, integrate vulnerability scanning into your CI/CD pipeline, and treat critical ORM upgrades with the same urgency as patching your own code.
When your ORM is the vulnerability, no amount of careful query writing will save you. Stay current, scan continuously, and let automated tools catch what manual review misses.