Skip to content

OD101 sql-injection

cr.execute() receives a query built by string formatting.

Category Severity Series Needs the index
Security Error all no

What it reports

A call to execute() on a cursor (cr, self.env.cr, self._cr) whose query is built in the call by %, +, .format() or an f-string from a value that is not a literal.

Why it matters

A value spliced into the query text becomes SQL. When it comes from a form, a URL, an import file or a record a user can edit, a quote in it ends the string and the rest runs as SQL. The query runs as the database user, so Odoo's access rights and record rules do not apply: the injected SQL can read or change any table. Passed as the second argument of execute(), values are quoted by the database driver and are never SQL. Table and column names cannot be passed that way; from 17.0, odoo.tools.SQL and SQL.identifier() compose them safely.

Values the model's own code produces are left alone: literals, self._table and other attributes of self, and calls such as self._select() without arguments, as report models use to assemble their view. Only the expression passed to execute() is read, with its template written there as a literal, so a query formatted on an earlier line or into a variable is not reported.

Where the value comes from a request or an RPC caller, OD701 reports the call instead, with the path the value takes, including a query formatted on an earlier line.

Example

models/res_partner.py
class ResPartner(models.Model):
    _inherit = "res.partner"

    def _count_orders_since(self, date_from):
        self.env.cr.execute(
            "SELECT COUNT(*) FROM sale_order WHERE partner_id = %s AND date_order >= '%s'"
            % (self.id, date_from)
        )
        return self.env.cr.fetchone()[0]

date_from becomes part of the SQL text, quotes and all.

models/res_partner.py
class ResPartner(models.Model):
    _inherit = "res.partner"

    def _count_orders_since(self, date_from):
        self.env.cr.execute(
            "SELECT COUNT(*) FROM sale_order WHERE partner_id = %s AND date_order >= %s",
            (self.id, date_from),
        )
        return self.env.cr.fetchone()[0]

Suppressing

self.env.cr.execute("ANALYZE %s" % table)  # odooscan: ignore[OD101]

See suppressing a finding.