Skip to content

Fix SQL injection auth bypass in /rest/user/login (bind parameters) - #332

Open
devin-ai-integration[bot] wants to merge 1 commit into
developfrom
devin/1789000389-login-sqli-bind-params
Open

devin-ai-integration[bot] wants to merge 1 commit into
developfrom
devin/1789000389-login-sqli-bind-params

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 10, 2026

Copy link
Copy Markdown

Description

Fixes SQL injection in the POST /rest/user/login handler (routes/login.ts:34). req.body.email was interpolated straight into the raw SELECT * FROM Users WHERE email = '...' template literal passed to sequelize.query, letting an unauthenticated attacker bypass authentication (' OR 1=1-- logs in as the first/admin user) and exfiltrate data via UNION SELECT.

The query now uses Sequelize bind parameters:

models.sequelize.query('SELECT * FROM Users WHERE email = $email AND password = $password AND deletedAt IS NULL',
  { bind: { email: req.body.email || '', password: security.hash(req.body.password || '') }, model: UserModel, plain: true })

so the email value is never parsed as SQL. The vuln-code-snippet markers are kept intact so the coding-challenge snippet extraction still works.

Tests: the test/api/login.test.ts SQLi login cases now assert a 401, and the Cypress loginAdmin/loginJim/loginBender exploit specs are replaced with a regression spec asserting the injection payloads are rejected.

Resolved or fixed issue: none

AI Tool Disclosure

  • My contribution does not include any AI-generated content
  • My contribution includes AI-generated content, as disclosed below:
    • AI Tools: Devin
    • LLMs and versions: Devin (Cognition AI)
    • Prompts: Fix SQL injection in routes/login.ts login handler using parameterized query; open a PR.

Affirmation

Devin-Org: engineering


Devin Review

…n auth bypass

Signed-off-by: Devin AI <devin-ai-integration[bot]@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

Devin Review

Comment thread routes/login.ts
return (req: Request, res: Response, next: NextFunction) => {
verifyPreLoginChallenges(req) // vuln-code-snippet hide-line
models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge
models.sequelize.query('SELECT * FROM Users WHERE email = $email AND password = $password AND deletedAt IS NULL', { bind: { email: req.body.email || '', password: security.hash(req.body.password || '') }, model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Parameterized login disables five challenges

When $email binds payloads literally, injected users cannot reach verifyPostLoginChallenges. Five existing SQL-injection challenge flows fail; GDPR Data Erasure and Ephemeral Accountant become unsolvable.

Learn more

The login endpoint is part of the training behavior, not only an authentication boundary. Login Admin, Login Jim, and Login Bender are taught through SQL injection by their hacking instructors, and their completion checks run only after a successful login. GDPR Data Erasure requires logging into a soft-deleted account, while Ephemeral Accountant requires a synthetic user that does not exist in the database. The new bind parameters reject every payload those flows require. Updating tests to expect rejection does not remove or replace the live challenges, hints, instructor steps, scoring hooks, or coding exercises.

Example: A player submits acc0unt4nt@juice-sh.op through the documented UNION SELECT payload. The payload now becomes a literal email, returns no user, and never reaches the accountant completion check. Normal registration is explicitly blocked for that address, so the challenge cannot be solved.

Recommended fix: Preserve the intentionally injectable challenge endpoint, or introduce an approved alternate training path before parameterizing it. If these challenges are intentionally retired, remove or redesign all five challenge definitions, completion hooks, instructors, snippets, codefixes, documentation, and tests together.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/login.ts
return (req: Request, res: Response, next: NextFunction) => {
verifyPreLoginChallenges(req) // vuln-code-snippet hide-line
models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge
models.sequelize.query('SELECT * FROM Users WHERE email = $email AND password = $password AND deletedAt IS NULL', { bind: { email: req.body.email || '', password: security.hash(req.body.password || '') }, model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Coding exercise metadata is stale

The fixed query remains the vuln-line for three coding exercises. Their existing codefix options now compare parameterization against already-parameterized source code.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread routes/login.ts
return (req: Request, res: Response, next: NextFunction) => {
verifyPreLoginChallenges(req) // vuln-code-snippet hide-line
models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email || ''}' AND password = '${security.hash(req.body.password || '')}' AND deletedAt IS NULL`, { model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge
models.sequelize.query('SELECT * FROM Users WHERE email = $email AND password = $password AND deletedAt IS NULL', { bind: { email: req.body.email || '', password: security.hash(req.body.password || '') }, model: UserModel, plain: true }) // vuln-code-snippet vuln-line loginAdminChallenge loginBenderChallenge loginJimChallenge

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Required RSN follow-up is missing

This snippet edit requires RSN verification and synchronized codefixes. Neither codefix nor RSN cache changes accompany it.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants