- Published on
Security Vulnerability Found in Heroku and Rails form_tag
- Authors
- Name
- Benjamin Manns
- @benmanns
Update: Heroku's Official Response
Last week I discovered and responsibly disclosed a vulnerability in the way that Heroku uses form_tag
to submit single sign-on credentials for their add-on providers. This vulnerability could be used to execute a CSRF attack on api.heroku.com on a targeted user's account and apps. The vulnerability was immediately patched but may be present in the way you or others use form_tag
.
Rails's Cross Site Request Forgery Protection
Rails uses a security token in non-GET requests to protect applications from request forgery. When you use form_tag
or form_for
, Rails adds a hidden element containing the security token:
<div style="margin:0;padding:0;display:inline">
<input name="authenticity_token" type="hidden" value="..." />
</div>
This value is POSTed to your server, and is checked against the stored value in the user's session to make sure that he or she is not being attacked.
The Problem
However, this tag is automatically inserted for every form_tag
call you make, even those to remote websites. So, if you are creating a search form:
form_tag('https://www.google.com/search') { text_field_tag 'q' }
yields:
<form accept-charset="UTF-8" action="https://www.google.com/search" method="post">
<div style="margin:0;padding:0;display:inline">
<input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden" value="..." />
</div>
<input id="q" name="q" type="text" />
</form>
But, Google has no need for and shouldn't have this access_token
.
The Vulnerability
Heroku uses a signed token to authenticate users to add-on providers with a POST request like:
POST https://yourcloudservice.net/sso/login
id=123&token=4af1e20905361a570×tamp=1267592469&nav-data=...&email=user@example.com
Where id
is the user's id, token is a SHA1 hash of id
, a secret, and the timestamp
. This form is generated at the https://api.heroku.com/myapps/:app/addons/:plugin
endpoint when a user clicks on an add-on link from the app dashboard. The form is automatically submitted using JavaScript, and the user is authenticated.
However, because Rails automatically inserts the token by default, the authenticity_token
was being sent to add-on providers.
The Proof of Concept
Upon discovering this, I created a proof of concept add-on (token-bandit) and vulnerable app (unsuspecting-victim). When I added the token-bandit add-on to the unsuspecting-victim app, I was successfully able to collect the authenticity_token
from my account and use it to add a new collaborator to the project.
Expansion of the Vulnerability
This attack vector requires that an 'unsuspecting victim' add the malicious add-on, because the https://api.heroku.com/myapps/:app/addons/:plugin
URL is locked to collaborators on the given :app
.
However, collaborators can be added to an app without their confirmation. Once added, being linked to the URL will trigger the exploit.
To add insult to injury, the bot@heroku.com
email account sends a notification to invited collaborators with a link to the app:
token-bandit@example.com has invited you to collaborate on their app “token-bandit” on Heroku: http://token-bandit.herokuapp.com/
Since you already have an account with Heroku, you can get started by simply git cloning the app repository:
git clone git@heroku.com:token-bandit.git -o heroku
See our quickstart guide for additional information: http://devcenter.heroku.com/articles/collab
An attacker could set up a redirect from http://token-bandit.herokuapp.com/
to the exploit URL, so when a target clicks on the app link in the Heroku email, they are exploited.
The Fix
Code Changes
In Rails version 3.1.0, an option was added to form_tag
so that you could specify authenticity_token: false
to disable the authenticity token in forms. This way,
form_tag 'https://www.google.com/search', authenticity_token: false do
text_field_tag 'q'
end
yields
<form accept-charset="UTF-8" action="https://www.google.com/search" method="post">
<div style="margin:0;padding:0;display:inline">
<input name="utf8" type="hidden" value="✓" />
</div>
<input id="q" name="q" type="text" />
</form>
protecting the authenticity_token
.
In Rails versions < 3.1.0, you have to create the form manually using ERB or HAML as follows:
%form{'accept-charset' => 'UTF-8', action: 'https://www.google.com/search', method: 'post'}
%div{style: 'margin:0;padding:0;display:inline'}
= hidden_field_tag 'utf8', '✓', id: nil
= text_field_tag 'q'
Session Reset
Because the default add-on provider API app created by the kensa gem logs SSO requests, I suggested that Heroku reset their session tokens. This way, if an existing add-on is compromised or has malicious intent, the attackers can't use previously logged authenticity tokens to exploit Heroku users. To do this in your own app, edit config/initializers/secret_token.rb
and replace the token in the ellipsis:
MyApp::Application.config.secret_token = '...'
Note that this will reset all of your users sessions, forcing them to login again, so use with caution.
Other Vulnerable Apps
While the widespread effect of this attack is negligible, as most apps do not POST data to arbitrary third parties, there is still a risk that trusted third parties (e.g. newsletter forms, search) could be compromised.
Further Work
I'm not sure the best way to resolve this moving forward. Adding a URL check to Rails seems like a good idea, so that any URL starting with http(s)://
rather than a local /
path doesn't send the authenticity_token
, but I know plenty of people are not careful with their use of _url
vs _path
helpers. Maintaining a whitelist of acceptable domains and parsing for them would require messy configuration. I will be submitting a pull request to Rails to discuss this issue and update the documentation for form_tag
.