Skip to content

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

controllers/main.py
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.

controllers/main.py
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.

Suppressing

request.env.cr.execute(query)  # odooscan: ignore[OD701]

See suppressing a finding.