How the Wrong Content Type Introduced a Vulnerability in Odoo
In this article, Sonar's R&D team will provide an overview of content types and how a minor error resulted in a Cross-Site Scripting vulnerability in Odoo.
Join the DZone community and get the full member experience.
Join For FreeAs a web developer, do you really know what content types are? Sure, something like text/html
should ring a bell, but are you also aware that getting them wrong can lead to security vulnerabilities in your application?
In this article, I will first give you a recap of what content types are and what they are used for. I will then show how important it is to get them right in your code by explaining how a small mistake led to a Cross-Site Scripting vulnerability in Odoo, a popular open-source business suite written in Python. Odoo has features for many business-critical areas, such as e-commerce, billing, or CRM, making it an interesting target for threat actors.
The vulnerability is tracked as CVE-2023-1434 and is caused by an incorrect content type being set on an API endpoint. Attackers could abuse it by crafting a malicious link that allows them to impersonate any victim on a vulnerable Odoo instance that clicks that link. If the victim has high privileges, attackers may be able to exfiltrate important business data. This bug is exploitable in the default configuration of Odoo; no addon is required.
Odoo maintainers addressed this vulnerability on December 23, 2022, and the fix is already part of the 16.0 release.
(If you are already up-to-speed on content types, feel free to jump to Diving Into CVE-2023-1434!)
Content Types?
The content type, also known as MIME type, is a crucial piece of information for web browsers. They need this information to display the server's response the right way.
It starts in the request, where the browser sets the Accept
header to tell the server what acceptable types are. For instance, when your browser requests a CSS stylesheet, it will likely attach Accept: text/css
. Your browser could also feel adventurous and send */*
(meaning, any type!), or send multiple values, each with a weight like q=0.1,
to give the server a choice.
The server can then use this value to decide on which Content-Type
header to attach to the response. It can also use values from the request path (i.e., extensions) to take this decision or simply ignore it.
Content Sniffing
In cases where the content type of a resource is not explicitly stated by one of the two sides, Content Sniffing usually kicks in. It means that an application has to decide on its own which type of content some unknown blob of data is, and yes, it is as likely to have the wrong result as it sounds.
Server-Side Content Sniffing
It can happen server-side, by a reverse proxy, or the application itself, when the developer specifies no content type. This process is error-prone and likely leads to unintended results. There are several documented examples of this going wrong. For instance, Simon Scannell exploited it in CVE-2021-39249 on Invision Power Board, where he could upload attachment files without extensions. However, by default, the Apache HTTP server will attach text/html
to files without extensions, letting Simon upload files later distributed as HTML documents.
The Go standard library has a very limited set of file extensions and their associated MIME types. In minimalistic environments of containers, i.e. based on alpine
, the system may not provide enough additional type definitions.
In this context, it is then likely that attackers could upload static files whose extension is allowed by the application but unknown by the Go server-side MIME sniffing feature. The file may then be served as text/html
and introduces a Stored Cross-Site Scripting vulnerability.
Client-Side Content Sniffing
It can also happen client-side, in the user's browser, when the response doesn't contain a Content-Type
header or an invalid one. The MIME sniffing algorithm is documented in a WHATWG living document and lists byte patterns to look for and the computed MIME type to attach if they are found in the response. For instance, the presence of <!DOCTYPE HTML
or <HTML
along with a character closing the tags raises text/html
, %PDF-
raises application/pdf,
and so on.
Yaniv Nizry identified a quirk in Apache's mod_mime
module, where files with extensions but an empty (.jpg
) or dot name (…jpg)
would be served without a content type. The browser would then "sniff" the content and could be tricked into rendering them as HTML documents.
With these examples, it is clear that Content Sniffing is here to accommodate users and always tries to show them valid pages in their browsers–not for security.
I even developed a rule as part of our Clean Code offering to remember telling browsers not to rely on it: Allowing browsers to sniff MIME types is security-sensitive. I suggest addressing it by setting the header X-Content-Type-Options
to nosniff
in all responses to tell browsers not to attempt content sniffing on the resources. It won't prevent cases where the content type is incorrectly stated.
What Could Go Wrong?
Let's take the example of an image returned with the wrong content type information, for instance, text/html
. The browser displays gibberish–the ASCII representation of the file's bytes:
But that also means that if there's any HTML tag in this file, they will be rendered by the browser. For instance, below, I have the result of the emoji in a <h1>:
Attackers could replace this tag with <script>
to include arbitrary JavaScript code instead. Executing such code in the victim's browser allows impersonating them on the same origin (as in "Same-Origin Policy").
Now that you have a good understanding of content types and why they can be security-relevant, you can look into a vulnerability my team and I found in Odoo.
Diving Into CVE-2023-1434
As part of the advanced features for developers, Odoo users can enable profiling for their session to identify potential performance bottlenecks in their application. They can later visualize flame graphs of their traces with a speedscope instance:
One of the ways to interact with the profiler is through an API handler, like /web/set_profiling/
. At [1]
, the decorator exposes it to /web/set_profiling
without authentication, at [2]
it creates the variable state with a call to set_profiling()
, and then at [3
] it returns a JSON-encoded output of this variable:
The method set_profiling()
is defined in ir_profile.py
. In the snippet below, at [1]
, [2]
, and [3]
, request.session
is populated with the method parameters profile
, collectors,
and params
. These values are then returned in a dict
:
@api.model
def set_profiling(self, profile=None, collectors=None, params=None):
# [...]
if profile:
# [...]
elif profile is not None:
# [1]
request.session.profile_session = None
if collectors is not None:
# [2]
request.session.profile_collectors = collectors
if params is not None:
# [3]
request.session.profile_params = params
return {
'session': request.session.profile_session,
'collectors': request.session.profile_collectors,
'params': request.session.profile_params,
}
So yes, my team and I have full control over them. URL parameters like profile=0
, collectors=<script>alert(document.domain)</script>
is enough to trigger the vulnerability. The resulting DOM, as seen by the client's browser, is as follows:
Note that, while the server does not send them, the browser added the html
, head
, and body
tags around the actual data because the server signaled that the response is an HTML page! Accessing the page is enough to trigger the JavaScript code:
Remediating Cross-Site Scripting Vulnerabilities
In the case of Cross-Site Scripting vulnerabilities, I believe that the best way of addressing these risks is at the very end of the chain: when displaying the data. Special characters must be made ineffective, whether by escaping or encoding them, but always depending on the context in which the data is injected.
For instance, JavaScript string literals and HTML support different escaping methods, and using the wrong one will likely introduce a Cross-Site Scripting vulnerability. Always make sure to know the context and use the most appropriate function.
The case of Odoo is a bit unusual. Common solutions would have been to implement a strict validation of the parameters or convert tags into HTML entities in the JSON string. Still, none of these should be considered satisfactory because the root cause boils down to this wrong content type: it must be addressed by setting the right content type on the API endpoint.
I also recommend investing in a strong Content Security Policy, which will not prevent vulnerabilities but make them harder or impossible to exploit. It always takes time and a few iterations to get it right, so the sooner, the better!
Patching CVE-2023-1434
Odoo maintainers addressed the vulnerability with ec8dd1a by adding an explicit content type, application/json
, on this endpoint.
If an UserError
exception is raised, the exception message is prefixed with error:
; this is not a valid JSON document. In that specific case, the maintainers set the content type to text/plain
to tell browsers not to render it.
diff --git a/addons/web/controllers/profiling.py b/addons/web/controllers/profiling.py
index b320ee0cfba4e..640f8b4e210fc 100644
--- a/addons/web/controllers/profiling.py
+++ b/addons/web/controllers/profiling.py
@@ -16,9 +16,9 @@ def profile(self, profile=None, collectors=None, **params):
profile = profile and profile != '0'
try:
state = request.env['ir.profile'].set_profiling(profile, collectors=collectors, params=params)
- return json.dumps(state)
+ return Response(json.dumps(state), mimetype='application/json')
except UserError as e:
- return Response(response='error: %s' % e, status=500)
+ return Response(response='error: %s' % e, status=500, mimetype='text/plain')
@route(['/web/speedscope', '/web/speedscope/<model("ir.profile"):profile>'], type='http', sitemap=False, auth='user')
def speedscope(self, profile=None):
Timeline
DATE | ACTION |
2022-12-22 | I report the vulnerability to the vendor. |
2022-12-23 | The vulnerability is fixed in ec8dd1a. |
2022-12-25 | Vendor informs my team and I that the SaaS platform is not vulnerable and that a fix is under validation. |
Summary
In short, getting the content type right is crucial for web developers to ensure the security of their applications. Client-side vulnerabilities can have a significant impact on the security of an application and should not be ignored.
Published at DZone with permission of Thomas Chauchefoin. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments