TL;DR
Trello supports PowerUps — integrations that connect third-party services to your board. One such PowerUp connects a popular external development platform, allowing users to link their external accounts and pull data (including private data) into Trello.
The PowerUp's authorization model is designed so that each user's connected external account is isolated — only you can see and interact with your own linked data. Other board members can't view, pull, attach, or remove your external data. They're prompted to connect their own account instead.
The problem: this isolation is only enforced on the client side (in the browser). At the API level, any board member can send requests to view, pull, attach, or remove any other user's connected external data — including private repositories, branches, commits, and files. The server doesn't verify that the requesting user owns the external data they're interacting with.
Background — PowerUps and Third-Party Integrations
Trello's PowerUp system lets you add third-party integrations to your boards. These integrations connect external services — code hosting platforms, design tools, cloud storage, CRM systems — to bring relevant data directly into your Trello workflow.
When a user connects their external account through a PowerUp, they go through an OAuth authorization flow — they log into the external service and grant Trello permission to access their data. This is standard: you've probably seen "Authorize [App] to access your [Service] account?" prompts before.
The critical detail: when a user authorizes Trello to access their external account, Trello gains access to both public and private data. This includes private repositories, private branches, internal code, confidential file names — anything the user has access to on the external platform. That's why the authorization is per-user: only the person who authorized the connection should be able to interact with that data through Trello.
The Permission Model
The intended permission model for this PowerUp is straightforward and sensible:
- Each user's connected account is isolated. When User A connects their external account, only User A can browse, pull, attach, and remove their data through the PowerUp.
- Other users are prompted to connect their own accounts. When User B opens the PowerUp settings, they don't see User A's data. Instead, they see a prompt to connect their own external account.
- No cross-user access. User B should have zero ability to interact with User A's connected data — not to view it, not to pull it into the board, not to attach it to cards, and not to remove it.
In the UI, this works correctly. When another user opens the PowerUp, they see a connect/authorize screen — no indication that another user has already linked their account, and no way to access that user's data through the interface.
Where It Breaks — Client-Side vs Server-Side
The isolation described above — "only the owner can interact with their data" — is enforced only on the client side. The browser JavaScript hides the other user's data and shows a "connect your own account" prompt instead. But the underlying API endpoints accept requests from any board member, regardless of who connected the external account.
This means that by crafting API requests directly (bypassing the UI), any board member can:
- View another user's connected external data
- Pull that data into the board
- Attach that data to cards visible to all board members
- Remove another user's connected data from the board
All of these actions use the attacker's own session cookies and CSRF token — no victim credentials needed. The attacker just references the victim's data through identifiers that are accessible to any board member.
Three Attack Vectors
The vulnerability manifests in three distinct attack scenarios, each escalating in impact. Since the vulnerability is unresolved, the specific API endpoints and request payloads are redacted.
Attack 1: Pulling Private External Data to the Board
The first attack allows a board member to pull another user's connected external data into the shared board — including data from private repositories that the attacker would normally have zero access to.
PUT /1/board/[BOARD_ID]/██████████/██████████████████████ HTTP/2
Host: trello.com
Cookie: <ATTACKER-COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
value=[REDACTED — contains victim's external account data]
&dsc=<ATTACKER-CSRF-TOKEN>
Response: 200 OK
The attacker uses their own cookies and CSRF token — not the victim's. The only "victim" data in the request is an identifier for the victim's connected account, which is accessible to any board member through a standard API call.
Attack 2: Attaching Private Data to Public Cards
The second attack goes further: the attacker can attach specific external data (branches, commits, files) from the victim's private account directly to Trello cards. These attachments become visible to every board member.
POST /1/cards/[CARD_ID]/attachments HTTP/2
Host: trello.com
Cookie: <ATTACKER-COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
url=[REDACTED — URL to victim's private external resource]
&name=[REDACTED]
&dsc=<ATTACKER-CSRF-TOKEN>
Response: 200 OK
This turns a broken access control issue into a private information disclosure vulnerability. The attacker can systematically attach data from the victim's private repositories — branch names, commit messages, file names, issue titles — to cards that are visible to all board members.
Attack 3: Removing the Victim's Connected Data
The third attack is destructive: the attacker can remove the victim's connected external data from the board entirely. After this, the victim's integration is broken — their linked data disappears, and they may need to re-authorize and reconfigure everything.
PUT /1/board/[BOARD_ID]/██████████/██████████████████████ HTTP/2
Host: trello.com
Cookie: <ATTACKER-COOKIES>
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
value=[REDACTED — empty payload that clears the victim's data]
&dsc=<ATTACKER-CSRF-TOKEN>
Response: 200 OK
This attack uses the same endpoint as Attack 1, but with a payload that clears the victim's data instead of pulling it. The server processes the destructive request without verifying that the attacker has authority over the victim's connected account.
Root Cause — Why This Happens
The root cause is a pattern we've seen across multiple Trello vulnerabilities: client-side authorization that isn't replicated on the server.
The PowerUp's frontend (JavaScript in the browser) correctly implements the isolation model. When User B loads the PowerUp settings, the UI hides User A's data and prompts User B to connect their own account. From a UI perspective, User B has no way to interact with User A's data.
But the API endpoints behind the PowerUp do not perform the same ownership check. They verify that the requester is a board member (a very broad check) but don't verify that the requester is the owner of the connected external account (the check that actually matters).
// What the UI does (client-side)
function showPowerUpSettings(user, board) {
const connectedAccount = getConnectedAccount(user.id, board.id);
if (!connectedAccount) {
showAuthorizationPrompt(); // "Connect your account"
hideOtherUsersData(); // User can't see anyone else's data
return; // ← UI-level isolation works ✓
}
showUserData(connectedAccount); // Only show THIS user's data
}
// What the API does (server-side)
function handlePluginDataUpdate(request) {
const user = authenticate(request.cookies);
if (!user) return 401;
const board = getBoard(request.boardId);
if (!board.hasMember(user.id)) return 403;
// ✗ Missing: is this user the OWNER of this plugin data?
// ✗ Missing: does this user have the right to modify THIS connected account?
updatePluginData(request.pluginDataId, request.value); // Accepts blindly
return 200;
}
The server checks: "Is this person a member of the board?" — yes. It does not check: "Is this person the owner of the specific external account they're trying to interact with?" That second check is the one that matters, and it's completely absent.
Impact
CVSS 3.0: AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N — 8.1 (High)
- Confidentiality: High — private external data (repository names, branch names, commit messages, file names, issue titles, private user information) can be disclosed to unauthorized board members
- Integrity: High — the attacker can modify the victim's PowerUp configuration (pulling and removing external data) without authorization
- Availability: Low — the attacker can disrupt the victim's integration by removing their connected data
What specific private data can be exposed:
- Private repository names — revealing what projects a user is working on
- Branch names — often containing ticket numbers, feature names, or internal project codenames
- Commit messages — potentially containing sensitive context about changes
- Private file and directory names — revealing codebase structure
- Issue titles — potentially containing vulnerability reports, security issues, or internal discussions
- Private collaborator names — revealing who has access to private projects
A Note on Bounty Scope
An interesting side story: the initial bounty payout was $600 instead of $1,200. The triage team classified this as a "first-party Trello app vulnerability" and applied a lower reward table.
I pushed back on this. The vulnerability is in Trello's API — every request in the report has Host: trello.com in the header. The external platform's servers are never contacted during the attack. Trello's endpoint accepts and processes the requests; Trello's server fails to check authorization. The bug is in Trello's code, not the external platform's.
After review, Atlassian agreed and updated the reward to $1,200, matching the standard P3 payout for the Trello target.
Host header in every request was sufficient to demonstrate that the vulnerability was in Trello's API, not in a third-party integration. Programs sometimes misclassify scope during triage — a polite, evidence-based response usually resolves it.Timeline
- 2024-12-10 — Vulnerability discovered and reported via Bugcrowd
- 2024-12-12 — Triaged as P3 (Broken Access Control > Privilege Escalation)
- 2024-12-12 — Initial bounty: $600 (classified as first-party app)
- 2024-12-13 — Scope challenge submitted — argued the bug is in Trello's API (Host: trello.com)
- 2024-12-19 — Bounty updated to $1,200 (reclassified as Trello target)
- Present — Unresolved — fix has not yet been deployed
Takeaways for Hunters
- PowerUps and integrations are high-value targets. Trello's PowerUp system is a massive attack surface — dozens of third-party integrations, each with their own data access patterns, each with their own authorization logic (or lack thereof). When you see a PowerUp that connects to an external service, ask: "Does the API enforce the same per-user isolation that the UI shows?" If the UI prompts you to connect your own account but the API lets you interact with someone else's, you have a bug.
- Client-side restrictions are not security. This is the third Trello vulnerability in this blog series where the UI correctly restricts an action but the API doesn't enforce it. If a feature hides data from you in the browser, always test whether you can access it by sending the request directly. The UI is a convenience layer, not a security boundary.
- Look for per-user data in shared contexts. Whenever a shared resource (a board) contains per-user data (connected external accounts), there's a potential isolation boundary to test. Can User A's data be accessed by User B? The more sensitive the per-user data (private repositories, credentials, personal information), the higher the impact if the boundary is broken.
- Three-dimensional access control testing. Don't just test "can I read other users' data?" Test the full CRUD spectrum: Can you Create (pull/add) data on behalf of another user? Can you Read (view) their data? Can you Update (modify/attach) their data? Can you Delete (remove) their data? Each operation might have different authorization behavior.
- Challenge scope classifications. If a bug bounty program misclassifies your finding's scope, provide evidence and push back. Check which server handles the vulnerable requests (the
Hostheader), which codebase contains the authorization gap, and which reward table should apply. A polite, factual response usually gets it resolved.