As 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 blog post, we will first give you a recap of what content types are and what they are used for. We 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!)
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.
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.
We also highly recommend reading Server-Side MIME Sniff Caused by Go Language Project Containerization by @RuiShang9.
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
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.
We 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. We suggest addressing it by setting the header
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, we have the result of the emoji in a
Attackers could replace this tag with
Now that we have a good understanding of content types and why they can be security-relevant, we can look into a vulnerability we 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
, the decorator exposes it to
/web/set_profiling without authentication, at
 it creates the variable state with a call to
set_profiling(), and then at
 it returns a JSON-encoded output of this variable:
Digging into Odoo's
Response implementation, we can see that it directly inherits from werkzeug's
Response, which is the underlying web framework:
default_mimetype is set to
text/html–very interesting! Indeed, werkzeug's default MIME type is originally set to
text/plain if the developer didn't override it in the constructor:
We are now in a situation where we are returning JSON data with a
text/html content type. But do we control parts of that data?
set_profiling() is defined in
ir_profile.py. In the snippet below, at
request.session is populated with the method parameters
params. These values are then returned in a
So yes, we have full control over them. URL parameters like
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
Remediating Cross-Site Scripting Vulnerabilities
In the case of Cross-Site Scripting vulnerabilities, we 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.
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.
We 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!
Odoo maintainers addressed the vulnerability with ec8dd1a by adding an explicit content type,
application/json, on this endpoint.
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.
(We will update this publication with a link to the official advisory as soon it is published).
|2022-12-22||We report the vulnerability to the vendor.|
|2022-12-23||The vulnerability is fixed in ec8dd1a.|
|2022-12-25||Vendor informs us that the SaaS platform is not vulnerable and that a fix is under validation.|
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.
We would like to thank Olivier Dony of Odoo S.A. for promptly deploying a patch and for their very effective communication.
Enjoy all things Python, and want more? Register now for our upcoming webinar Clean Code for your Python projects, with Nafiul Islam - Wednesday, May 10th - 5 PM CEST / 10 AM CDT.