Skip to content

fix: fix $bind and :repl being replaced in user input (v7)#14447

Merged
ephys merged 71 commits into
mainfrom
ephys/fix-13817
Apr 28, 2022
Merged

fix: fix $bind and :repl being replaced in user input (v7)#14447
ephys merged 71 commits into
mainfrom
ephys/fix-13817

Conversation

@ephys
Copy link
Copy Markdown
Member

@ephys ephys commented Apr 24, 2022

This PR is still WIP but I'm opening it to describe the changes as I go.

Fixes in v7: #13817
Closes #14273
Closes #12523

This (v7) PR fixes how bind & replacements are handled in query interface & findAll methods.

Before this PR

Let's take the following query:

User.findAll({
  where: and(
    literal('soundex("firstName") = soundex(:firstName)'),
    { lastName: lastName },
  ),
  replacements: { firstName },
})

Before this PR, Sequelize would first generate the SQl for the where parameter:

SELECT * FROM users WHERE soundex("firstName") = soundex(:firstName) AND "lastName" = 'Doe'

Then pass that query to Sequelize#query, which would process the replacements option (in this case, the :firstName parameter):

SELECT * FROM users WHERE soundex("firstName") = soundex('John') AND "lastName" = 'Doe'

Because of this two-step processing, a malicious input can result in SQL injection. For instance with these two user inputs:

const lastName = ':firstName'; // <- notice how it includes the syntax for a replacement parameter
const firstName = '; DROP TABLE users;'

With the above input, QueryGenerator will first generate this:

SELECT * FROM users WHERE soundex("firstName") = soundex(:firstName) AND "lastName" = ':firstName'

Then Sequelize#query will process :firstName, resulting in this:

SELECT * FROM users WHERE soundex("firstName") = soundex('; DROP TABLE users;') AND "lastName" = ''; DROP TABLE users;''

Resulting in the injection of DROP TABLE users;, interpreted as actual SQL.

After this PR

Replacements are now handled by QueryGenerator instead. This allows QueryGenerator to only process replacements in literal() and nowhere else.

QueryGenerator will generate this:

SELECT * FROM users WHERE soundex("firstName") = soundex('; DROP TABLE users;') AND "lastName" = ':firstName'

Then the query will be passed to a new method, Sequelize#queryRaw, which does not process replacements at all. So the query is executed as-is.

Sequelize#query will still process replacements, but as long as you don't put replacements inside strings there, and don't inline user input yourself, you'll be fine.

Escaping $params

It is not necessary to escape $bind parameters in string anymore. This solution was a horrible band aid that caused more issues than it solved.

Only bind parameters outside of strings and comments will be treated as bind parameters by Sequelize.

Example 1 (mysql):

const result = await this.sequelize.query(`select * from users WHERE id = '$$one'`);
-- before this PR, it was executed as:
SELECT * FROM users WHERE id = '$one';

-- after this PR, it is executed as:
SELECT * FROM users WHERE id = '$$one';

Example 1 (mysql):

const result = await this.sequelize.query(`select * from users WHERE id = '$one'`);
-- before this PR, it was executed as:
SELECT * FROM users WHERE id = '?';

-- after this PR, it is executed as:
SELECT * FROM users WHERE id = '$one';

Other changes

  • The bind & replacement options can now be used at the same time! This is because handling of bind parameters is done by a new parser that knows whether $name is a bind parameter, or just text inside a string.
  • It's now possible to use bind parameters in update, and insert methods. It was not previously possible because sequelize generates its own bind parameters in these methods, which would overwrite the ones provided by the user. They are now safely merged.
  • ::cast is not interpreted as a replacement anymore, it's a cast.

TODO

  • Test the new bind replacer against comments
  • Finish updating the tests

TODO (future PR)

@ephys ephys marked this pull request as draft April 24, 2022 15:22
@ephys ephys requested a review from WikiRik April 27, 2022 13:53
WikiRik
WikiRik previously approved these changes Apr 27, 2022
Copy link
Copy Markdown
Member

@WikiRik WikiRik left a comment

Choose a reason for hiding this comment

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

New test looks good and other comments have also been addressed so I think this is good to go

@ephys
Copy link
Copy Markdown
Member Author

ephys commented Apr 27, 2022

There is one last fix I want to add before merging.
Now that we have a string-aware SQL "parser", we can prevent replacements from being transformed in literals too literal('= \':value\'') should not consider :value to be a replacement

@ephys ephys requested a review from WikiRik April 28, 2022 10:00
@ephys
Copy link
Copy Markdown
Member Author

ephys commented Apr 28, 2022

My latest commit makes replacements use the same string & comment aware SQL parser as bind parameters.

This change fixes a bug where the following query would throw because no value was provided for :id, even though it's not a replacement because it is in a string.

User.findAll({
  where: literal(`id = ':id'`),
})

I think this is the solution I can backport to v6 without introducing a breaking change. Hopefully.

Copy link
Copy Markdown
Member

@WikiRik WikiRik left a comment

Choose a reason for hiding this comment

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

In general looks good, I have three comments about the tests. Same comments also apply to their respective equivalent for positional replacements

Comment thread test/unit/utils/sql.test.ts Outdated
Comment thread test/unit/utils/sql.test.ts
Comment thread test/unit/utils/sql.test.ts Outdated
Comment thread test/unit/utils/sql.test.ts
@ephys ephys requested a review from WikiRik April 28, 2022 15:15
Comment thread test/unit/utils/sql.test.ts Outdated
@ephys ephys requested a review from WikiRik April 28, 2022 18:03
Copy link
Copy Markdown
Member

@WikiRik WikiRik left a comment

Choose a reason for hiding this comment

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

Thanks again for your thorough work on this!

@ephys ephys merged commit 19ca248 into main Apr 28, 2022
@ephys ephys deleted the ephys/fix-13817 branch April 28, 2022 18:19
@ephys
Copy link
Copy Markdown
Member Author

ephys commented Apr 28, 2022

Now onto v6

@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 7.0.0-alpha.12 🎉

The release is available on:

Your semantic-release bot 📦🚀

vanthome pushed a commit to vanthome/sequelize that referenced this pull request Jun 12, 2022
…lize#14447)


BREAKING CHANGE: There is no need to escape $bind parameters in strings anymore, bind parameters & replacements are only treated as such where an SQL value can be used.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change For issues and PRs. Changes that break compatibility and require a major version increment. released on @v7 type: bug DEPRECATED: replace with the "bug" issue type

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

$ sign in fn method causing issues Escaped $ signs throw bind parameter errors when they should not

3 participants