-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
fix: do not replace :replacements
inside of strings
#14472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are good to go
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like nothing weird got backported
I just hope this doesn't introduce a breaking change, but in theory it will only be a breaking change if the feature was used 'incorrectly' (used a replacement in a place where a replacement should not be treated as one, like inside of a string). |
I will skip the mysql 8 tests: It would seem we are experiencing the same issue as we did with mariadb #14174 |
🎉 This PR is included in version 6.19.1 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This just broke my deployment, so it's a breaking change for me at the very least. "in theory it will only be a breaking change if the feature was used 'incorrectly' (used a replacement in a place where a replacement should not be treated as one, like inside of a string)" Not sure what you mean by replacements being used incorrectly inside a string. The docs do this:
|
Please, if you break things in a minor release update then please provide an example how to use replacements with raw queries. Why is it incorrect usage? |
By 'using a replacement inside a string' i meant a sql string, something like this; await sequelize.query(
`SELECT * FROM projects WHERE status = ':status'`,
{
replacements: { status: 'active' },
type: QueryTypes.SELECT
}
); Note the quotes around Your usage is correct and should not have broken. We'll look into what happened there. |
this is my usage, I use it to call sql functions, I don't have any strings there const res: bulkItem[] = await database.query(`SELECT "portfolioInvestmentId", value FROM ${metric}(:fundId, array[:pciIds]::uuid[], :untilDate, :excludeEventId)`, {
type: QueryTypes.SELECT,
replacements: {
fundId,
pciIds,
untilDate: dateToStr(untilDate),
excludeEventId,
}, |
This results in SELECT "portfolioInvestmentId", value FROM table('abc', array[:pciIds]::uuid[], 'ijk', 'lmn') Looks like the culprit is |
PR: #14518 |
I had 2 cases where using replacements broke. The first was with using it with LIMIT doing something like the below await sequelize.query(
'SELECT * FROM projects WHERE status = :status LIMIT :first',
{
replacements: { status: 'active', first: 20 },
type: QueryTypes.SELECT
}
); The 2nd case was with using it with an array as well, such as |
I just ran it against our latest v6 and it resulted in There may be something more going on. If you'd rather avoid posting the real query in public, you can send it to me privately on our slack (if you're comfortable with that), and I'll look into it. Edit: It's resolved :) |
Sadly this wasn't marked as breaking changes. It breaks my usage, where I previously used for
In this case, the variable needs to be inside string Any suggestion how to solve this |
We apologize for that but unfortunately that is considered to be a bug that was severe enough to warrant patching in all versions as it lead to SQL injections. In your case, unless you ensured that await sequelize.query(
`CASE WHEN ("Appointments"."canceledAt" >= ("Appointments"."startDate" - INTERVAL '${cutOffPeriod} hours')) THEN 'cancelled < ${cutOffPeriod} hours'`,
{
type: QueryTypes.SELECT
}
); Or you could do this, which is safe: await sequelize.query(
`CASE WHEN ("Appointments"."canceledAt" >= ("Appointments"."startDate" - INTERVAL :cutOff1)) THEN :cutOff2`,
{
replacements: {
cutOff1: `${cutOffPeriod} hours`,
cutOff2: `cancelled < ${cutOffPeriod} hours`,
},
type: QueryTypes.SELECT
}
); |
@ephys i see, if thats the concern that make sense to patch such thing. In my case, it's all constructed internally, so it wasn't really a problem. And thanks for the solution! That'll work. |
Here are two more cases where the replacement is not detected, with PostgreSQL.
select ('{"abc": 1}'::jsonb)->>:key;
do $$
begin
if :value then
refresh materialized view abc;
end if;
end
$$;
select 1; We worked around these cases by concatenating strings, and taking care of sanitization ourselves. |
Thank you! we'll look into fixing these two :) According to postgres' documentation, |
@ephys I'm clearing up my notifications and trying to ping you in some of those so they're back on your radar |
@@ -1,5 +1,5 @@ | |||
export type Escapable = undefined | null | boolean | number | string | Date; | |||
export function escapeId(val: string, forbidQualified?: boolean): string; | |||
export function escape(val: Escapable | Escapable[], timeZone?: string, dialect?: string, format?: string): string; | |||
export function escape(val: Escapable | Escapable[], timeZone?: string, dialect?: string, format?: boolean): string; | |||
export function format(sql: string, values: unknown[], timeZone?: string, dialect?: string): string; | |||
export function formatNamedParameters(sql: string, values: unknown[], timeZone?: string, dialect?: string): string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this wasn't updated, so existing TS users won't see the new injectReplacements function, nor will they see the deprecation.
@Caerbannog I haven't been able to resolve the The problem is that I cannot guarantee that the code injected inside of the string is safe. It requires a much more complex escape function. In your example, if I set do $$
begin
if '$$' then
refresh materialized view abc;
end if;
end
$$; Which is a syntax error because postgres considers I don't see a way to escape This limitation wasn't introduced by this PR, so that code was vulnerable before this PR, but I can't enable it until we've figured out a way to safely escape the value that needs to be injected inside of the string. |
Hi @ephys With earlier sequelize versions, passing |
|
||
const remainingString = sqlString.slice(i, sqlString.length); | ||
|
||
const match = remainingString.match(/^:(?<name>[a-z_][0-9a-z_]*)(?:\)|,|$|\s|::)/i); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This regex does not take into account an ending ;
on the statement, so I corrected to remainingString.match(/^:(?<name>[a-z_][0-9a-z_]*)(?:\)|,|\;|$|\s|::)/i);
.
Then, I discovered it does not like when the sql statement has JSON in it such as: UPDATE table SET value = value || '{"column::form": :formTemplateId}' where key = 'formTemplates.defaultForms'
(with replacements for :formTemplateId
passed in). It was not capturing the named parameter inside the jsonb update, :formTemplateId
. I fixed this by moving this block (lines 134-166) up to above line 53 (if (isString)
). I also had to update the regex here again, to include the option for the ending json block: remainingString.match(/^:(?<name>[a-z_][0-9a-z_]*)(?:\)|,|\;|\}|$|\s|::)/i);
.
Would there be any reason why we would not want to do the named parameter check before the other char
checks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The second part is not a bug, your :formTemplateId
is inside of a SQL string so we won't replace it because that will lead to sql injection as well as the risk of mangling data. You need to build your entire string before replacing it, or concat the replaced value to the rest of your string using the SQL concat function
The ;
thing may be a bug, I'd need an example of a failing query to make sure
This is also how it broke us. It is often not uncommon in javascript to pass undefined in as positional arguments. It's worse that this slipped in as not a major breaking change - this is a pretty major, breaking API change. |
Pull Request Checklist
yarn test
oryarn test-DIALECT
pass with this change (including linting)?Description Of Change
This PR backports the non-breaking changes from #14447 in v6
Only replacements have been backported, as unfortunately an escape hack was put in place to handle the bind parameter scenario, and reverting it would be a breaking change.
Closes #13817