Skip to content

OD111 public-route-sudo

A public or auth='none' route escalates with sudo().

Category Severity Series Needs the index
Security Info all no

What it reports

A controller method whose @http.route sets auth="public" or auth="none" and whose body calls sudo(). This is a hotspot: code to review, not a confirmed flaw.

Why it matters

A public route answers anyone, signed in or not; auth="none" runs without any user. sudo() lifts access rights and record rules, so from that point the code alone decides what the visitor may read or change. When a record id, a domain or values come from the URL or the form, the route must check that the visitor is entitled to that record, for example with an access token, or anyone can walk through the ids and read or modify every record. Many public routes use sudo() correctly; the finding asks for that check to be confirmed.

Routes with auth="user", the default, are not reported: Odoo's access rights still apply to the signed-in user until the code calls sudo(). Only the route's own body is read, so a sudo() in a helper it calls is not reported.

Example

controllers/main.py
class WarrantyController(http.Controller):
    @http.route("/warranty/<int:claim_id>", type="http", auth="public", website=True)
    def warranty_claim(self, claim_id):
        claim = request.env["warranty.claim"].sudo().browse(claim_id)
        return request.render("warranty_portal.claim_page", {"claim": claim})

Any visitor can open /warranty/1, /warranty/2 and so on and read every claim.

controllers/main.py
from odoo.addons.portal.controllers.portal import CustomerPortal


class WarrantyController(CustomerPortal):
    @http.route("/warranty/<int:claim_id>", type="http", auth="public", website=True)
    def warranty_claim(self, claim_id, access_token=None):
        try:
            claim = self._document_check_access("warranty.claim", claim_id, access_token)
        except (AccessError, MissingError):
            return request.redirect("/my")
        return request.render("warranty_portal.claim_page", {"claim": claim})

With warranty.claim inheriting portal.mixin, the portal checks the visitor's access or the record's token before it returns the record.

Suppressing

def warranty_claim(self, claim_id):  # odooscan: ignore[OD111]

See suppressing a finding.