CVE-2008-7248: Bypassing CSRF Protection with text/plain in Ruby on Rails
When designing security mechanisms, developers often rely on assumptions about how client applications — typically web browsers — will behave. We build our defenses around standard protocols and common data formats, expecting the client to play by the rules we’ve anticipated. However, when those assumptions are incomplete, the resulting blind spots can lead to severe vulnerabilities.
A prime example of this phenomenon is CVE-2008-7248, a vulnerability in Ruby on Rails ActionPack that allowed attackers to bypass Cross-Site Request Forgery (CSRF) protection simply by changing the content type of their malicious request.
This vulnerability, which affected Rails 2.1.x (before 2.1.3) and 2.2.x (before 2.2.2), demonstrates why security checks must be universally applied rather than selectively enforced based on expected request formats.
The Mechanics of Cross-Site Request Forgery
Before we examine the vulnerability itself, let’s briefly review how CSRF attacks work and how Rails attempts to prevent them.
In a CSRF attack, a malicious website instructs the victim’s browser to send a request to a different website where the victim is already authenticated. Because the browser automatically includes session cookies with the request, the target application believes the victim intentionally performed the action.
To mitigate this, Rails introduced the protect_from_forgery mechanism. When enabled, Rails generates a unique, unpredictable authenticity_token and embeds it in every generated HTML form. When the user submits the form, Rails verifies that the token in the request parameters matches the token stored in the user’s session. If the tokens don’t match, or if the token is missing, Rails rejects the request.
The Flaw: Selective Verification
The theory behind the Rails CSRF protection was sound. However, the implementation in Rails 2.1 contained a critical optimization flaw.
The Rails core team recognized that not all requests needed CSRF protection. For example, GET requests should be idempotent — meaning they shouldn’t change server state — so checking tokens on GET requests was unnecessary. Furthermore, they reasoned that APIs accepting formats like JSON or XML were typically accessed by programmatic clients, not web browsers, and therefore weren’t susceptible to traditional browser-based CSRF attacks.
To implement this logic, ActionController included a verifiable_request_format? method. This method checked the Content-Type of the incoming request. If the content type was considered “unverifiable” (like application/json or text/plain), Rails would simply skip the authenticity token check entirely.
The relevant code path in ActionController looked something like this:
def verified_request?
!protect_against_forgery? ||
request.method == :get ||
!verifiable_request_format? ||
form_authenticity_token == params[request_forgery_protection_token]
end
def verifiable_request_format?
request.content_type.nil? || request.content_type.verify_request?
end
The verify_request? method on the Mime::Type object would return false for formats like text/plain. Consequently, !verifiable_request_format? would evaluate to true, and the entire verified_request? check would succeed without ever looking at the authenticity token.
The text/plain Exploit Vector
The assumption that browsers only submit forms using standard formats like application/x-www-form-urlencoded or multipart/form-data was incorrect. HTML forms actually support a third encoding type: text/plain.
An attacker could craft a malicious webpage with a hidden form targeting the vulnerable Rails application:
<form action="https://vulnerable-app.com/users/delete_account" method="POST" enctype="text/plain">
<input type="hidden" name="user_id" value="123">
<input type="submit" value="Click here for a prize!">
</form>
When the victim clicked the button (or if the form was submitted automatically via JavaScript), the browser would send a POST request to the Rails application with a Content-Type of text/plain.
Because Rails saw a text/plain request, it categorized the format as unverifiable. It skipped the CSRF token check, processed the parameters, and executed the destructive action using the victim’s session. The CSRF protection was entirely bypassed.
The Resolution
The Rails core team addressed CVE-2008-7248 in versions 2.1.3 and 2.2.2. The fix required a shift in perspective. Instead of allowing arbitrary formats to bypass protection, the framework needed to protect against any format that a web browser could natively generate.
The developers introduced a new concept: browser_generated_types. This list explicitly included formats that standard HTML forms could produce, notably adding text/plain to the list of formats requiring strict verification. They modified the Mime::Type class to ensure that any request capable of being generated by a browser without the use of advanced techniques (like XHR with custom headers) was subject to the authenticity token check.
If you are maintaining a legacy application, the permanent solution is, of course, to upgrade to a supported version of Rails. If an immediate upgrade is impossible, you must manually enforce CSRF checks on all non-GET requests regardless of their content type, or explicitly reject text/plain POST requests at the web server level.
Long-Term Implications
CVE-2008-7248 provides a valuable lesson in defensive programming. Security mechanisms should generally fail closed, not open. When Rails encountered a content type it didn’t strictly associate with a browser form, it defaulted to trusting the request. A more robust — though perhaps less convenient — approach would have been to require the authenticity token for all state-changing requests unless explicitly bypassed by the developer.
This vulnerability also highlights the risk of relying on implicit assumptions about the broader web ecosystem. The HTTP and HTML specifications are vast, and browsers often support obscure features (like enctype="text/plain") that developers might not anticipate.
When working with legacy codebases, we must recognize that the security context shifts over time. A system that was perfectly secure against the known vectors of 2008 may be trivially exploitable today as new browser behaviors and attack techniques are discovered. Maintaining software sustainability requires not just writing good code, but actively updating our dependencies to incorporate the hard-won security lessons of the past decade.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming