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="&#x2713;" />
    <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&timestamp=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="&#x2713;" />
  </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', '&#x2713;', 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.