OD701 sql-injection-flow¶
Data a user controls reaches the text of an SQL query.
| Category | Severity | Series | Needs the index |
|---|---|---|---|
| Security | Error | all | no |
What it reports¶
A value from a source that reaches
the query text of cr.execute() (on cr, self.env.cr or self._cr) or the template of
odoo.tools.SQL() through formatting, concatenation, f-strings, locals or the methods it is passed
to, without a sanitizer such as int() or SQL.identifier() on the way. The message names the
source and each local and method the value went through; SARIF, GitHub and GitLab output give the
line of each step.
OD101 reports any query formatted inside the call. Where this rule reports the same call, OD101 does not.
Why it matters¶
A route parameter is set by anyone who can open the URL, and the argument of a public model method
by any user with access to the model, through /web/dataset/call_kw. Spliced into the query, a
quote in it ends the string and the rest runs as SQL, as the database user: access rights and
record rules do not apply, so the injected SQL can read or change any table. Passed as the second
argument of execute(), or as an argument of SQL(), the value is quoted by the database driver
and is never SQL.
Example¶
class OrderSearch(http.Controller):
@http.route("/shop/orders/search", type="http", auth="user")
def search(self, term):
where = "name ILIKE '%%%s%%'" % term
request.env.cr.execute("SELECT id FROM sale_order WHERE " + where)
ids = [row[0] for row in request.env.cr.fetchall()]
return request.render("shop_search.results", {"orders": request.env["sale.order"].browse(ids)})
term comes from the URL and becomes part of the SQL text, through where.
class OrderSearch(http.Controller):
@http.route("/shop/orders/search", type="http", auth="user")
def search(self, term):
orders = request.env["sale.order"].search([("name", "ilike", term)])
return request.render("shop_search.results", {"orders": orders})
search() passes term as a parameter, and applies the user's record rules as well.