How Odoo's dbfilter regex could once be injected through the Host header to defeat database filtering, a ReDoS variant we found on Odoo's own runbot, and the fix Odoo shipped after we reported it.
In a previous article, we saw that Odoo’s database filtering relies on a regular expression. Here we look at how, until it was fixed, that regex could be abused in some Odoo deployments, including Odoo’s own runbot.
The issue described below was reported to Odoo and has since been patched. It is covered here as a lesson in how a small piece of configuration can turn into a security weakness, not as a working attack: the versions concerned are long superseded.
The Vulnerability: Regex Injection
The %d and %h variables were replaced directly with values derived from the Host header of the HTTP request.
An attacker could craft a Host header to inject an arbitrary string into the regex, including special characters. By injecting |^.*$|, for example, they could change the behavior of the regex completely: every database name would suddenly match, defeating the filter.
Exploitation on Runbot
Wildcard DNS
Odoo has a wildcard DNS record for *.runbotXX.odoo.com. Take this runbot build as an example:
http://315285-10-0-opw-1820081-refix-sig-fc659d.runbot11.odoo.com
That hostname resolves as an alias for runbot11.odoo.com.
dbfilter
The Odoo instance for this build ran with --db-filter='%d.*$'. Visiting the build’s URL listed every database matching 315285-10-0-opw-1820081-refix-sig-fc659d.*$.
Wildcard Hostname Matching
On the Nginx side, the instance was reachable by every hostname matching ^315285-10-0-opw-1820081-refix-sig-fc659d[-.].*$.
So a hostname such as 315285-10-0-opw-1820081-refix-sig-fc659d-hello-my-name-is-brian.runbot11.odoo.com also matched. No database matched that name, though, so visiting it simply triggered Odoo’s database-creation form.
Special Characters
Because the .* part of the Nginx regex accepts any character, a hostname containing |^.*$| would match too. Such characters cannot appear in a DNS entry, so the request has to connect to runbot11.odoo.com while setting the Host header manually, which is straightforward with a normal HTTP client.
Listing All Databases
Combining these pieces bypassed the filter: with the crafted Host header, the database manager returned the full list of databases on the host, around 148 in the example.
The information disclosed here (a list of database names) was not especially interesting on its own. In another context, though, an attacker might chain it further: test databases often have weaker credentials than production ones, and a foothold in one could be used to work toward the rest of the host.
One Step Further: ReDoS
There is a class of attacks called Regular Expression Denial of Service (ReDoS), where a program is slowed to a crawl through a regex alone. Some patterns produce an exponential number of paths to evaluate against certain inputs.
The usual case is a vulnerable regex fed a malicious input. Here it was the reverse: we did not control the input (the database names Odoo filters), but we did control the regex, through the injected Host header. So instead of finding an input that hangs a regex, we looked for a regex that hangs on the existing database names. It needed two properties:
- a weak pattern such as
(a+)+; database names allow[0-9a-z\-], so we used([0-9a-z\-]+)+; - no match against any real database name, to force the engine through every path. Each build creates a
-baseand an-alldatabase, so names always end inbaseorall, which is easy to exclude.
Sending such a crafted regex through the Host header made the server spend all its time on the match. On runbot the request was killed after 60 seconds, because builds run in multi-worker mode with the default limit_time_cpu = 60. On a deployment without that limit, the effect would last longer.
Conclusion
We reported the issue to Odoo’s security team. Despite the limited impact, they chose to publish a full security advisory. The fix was trivial: escape the %d and %h variables with re.escape before building the regex (odoo/odoo#32511).
The wider lesson is that a value taken from an HTTP request, here the Host header, should never be dropped straight into a regular expression. Odoo followed its responsible disclosure process throughout, and the fix is in every current version.
A Note on OCA’s dbfilter_from_header Module
OCA’s dbfilter_from_header module contained a static/ directory, which had the unintended effect of loading it unconditionally, even when it was neither listed in server_wide_modules nor installed. So simply having it in the addons path left an instance exposed to the same ReDoS through the X-Odoo-dbfilter (or X-Openerp-dbfilter) header. That has since been addressed as well.