fix(mysql)!: use index identifiers instead of raw SQL in QB.useIndex()#12344
Conversation
Escape user-provided identifiers using the driver's escape method to prevent SQL injection via useIndex() table/index names and setLock() lockTables parameter. Fixes typeorm#12333
Code Review by Qodo
1.
|
smith-xyz
left a comment
There was a problem hiding this comment.
changes look good - could add a test around this behavior in test/functional/query-builder/sql-injection/
Add tests in test/functional/query-builder/sql-injection/ that verify: - useIndex() escapes malicious index names (MySQL) - useIndex() handles comma-separated index names by escaping each individually - setLock() lockTables escapes malicious table names (Postgres) Also fix useIndex to split comma-separated index names and escape each one individually, and update the existing useIndex test assertion to expect the escaped (backtick-wrapped) identifier.
|
@smith-xyz Thanks for the review! I've added SQL injection tests in
I also addressed the Qodo review feedback: |
|
|
Persistent review updated to latest commit 9984aaa |
smith-xyz
left a comment
There was a problem hiding this comment.
lgtm! thank you for your contribution
commit: |
|
Thanks @eddieran for your great help 🙏 |
| throw new TypeORMError("lockTables cannot be an empty array") | ||
| } | ||
| lockTablesClause = " OF " + this.expressionMap.lockTables.join(", ") | ||
| lockTablesClause = " OF " + this.expressionMap.lockTables.map((table) => this.escape(table)).join(", ") |
There was a problem hiding this comment.
Possible edge case: using table paths (and similar for indices). Both tables and indices (basically any database object) can be referenced by a path: "SCHEMA"."MY_TABLE".
IMO this is not a blocker to merge, both of these functionalities (useIndex/lockTables) are almost never used.
|
Persistent review updated to latest commit 9500618 |
|
Persistent review updated to latest commit 3a154e6 |
| if (DriverUtils.isMySQLFamily(this.dataSource.driver)) { | ||
| useIndex = ` USE INDEX (${this.expressionMap.useIndex})` | ||
| useIndex = ` USE INDEX (${this.expressionMap.useIndex | ||
| .split(",") |
There was a problem hiding this comment.
One thing: we should find a way to not split by ,. Maybe expressionMap.useIndex should be string[] and we should define SelectQueryBuilder.useIndex(...indexes: string[])/SelectQueryBuilder.useIndex(indexes: string|string[]) 🤷🏻♂️
|
@eddieran can you check on the tests - I think it could be related qodo's no.2 finding. Might need to update the applyFindOptions path to pass tableAlias.name instead of this.escape(tableAlias.name) to setLock()? e.g. its triple quoting in there |
|
It's double escaped because in In this case, I think both should be quoted in the The current escaping behavior in Right now I'm more interested in changing the signature of Ideally we should not accept issues that list more than 1 point (the linked one has 4 points and e.g. point #3 is irrelevant, we need to handle these things independently from each other) nor PRs that solve more than 1 issue (this one has 2, would have been easier to handle them independently). |
|
Re: comma-split — agree that's the right fix. Switching Re: schema-qualified paths ( Re: smith-xyz's |
|
@eddieran after internal discussion here's how we want to handle this:
Let us know if you'll have time to do that as we'd like to get the |
Revert the setLock lockTables escaping changes; this fix will land separately. Keep the useIndex escaping fix and shift its signature to accept a string or string[].
|
@eddieran I went ahead and pushed some changes as described. Hope that's ok and thanks for your work so far. |
|
the upgrade guide for MySQL needs a small update (only place where you can use |
QB.useIndex()



Summary
Fixes #12333. Replaces #12336 which was accidentally closed when the fork was deleted.
User-provided identifiers passed to
useIndex()andsetLock()lockTableswere interpolated directly into SQL strings without escaping, allowing SQL injection.Changes
useIndex(): Wrap the index name withthis.escape()so it is quoted with the driver's identifier escape character (e.g. backticks for MySQL).setLock()lockTables: Wrap each table name withthis.escape()before joining into theOFclause (Postgres).Both changes use the existing
QueryBuilder.escape()method which delegates to the driver's escape implementation, consistent with how all other identifiers are handled throughout the query builder.