On August 21, 2017, something of great cosmological significance occurred. Yes, sure, there was a total solar eclipse over wide swaths of the United States, but we’re talking about something life-changing here. On that fateful day, Alleybot quoted his first-ever message. What does that mean? Let’s discuss.
UPDATE 2019-03-12: Slack rolled out a change to the users.list API endpoint on Friday March 8, 2019 to filter responses for guests. Guest account calls to that endpoint now only receive profile information for users in their channels 🎉
This change seems limited to the users.list endpoint. Guest calls to users.profile.get are still able to query users inside and outside the channel to retrieve profile fields (including hidden custom fields 😬 – special thanks to Eric Miller for the confirmation)
UPDATE 2019-03-13: While we’re thrilled to see this patched, we can’t help noticing the timing of the fix. We reached out to Slack support over 6 months ago. We kept quiet and proactively asked for updates. Eventually, we warned Slack that we had drafted a public disclosure, and then we still sat on the post quietly for months after they asked us to give them more time. The issue was fixed the day after our post went live on our blog and Medium.
/shrug Interesting timing
At Alley we’re committed to transparency, and this has been a lesson for us in the risk/value of responsible public disclosure. In the future, we’ll be more strict about a 90-day disclosure window (and utilize any appropriate disclosure services that support that window).
TLDR; an unpatched Slack API behavior allows single or multi-channel guests to query your entire Slack Workspace directory including names, titles, emails, phone numbers, administration status, inactive/deleted accounts, custom profile fields, and even other single-channel guests from different channels.
Alley’s bread-and-butter expertise isn’t in pentesting or security-auditing (it’s in large scale development and design), but one can hardly escape a responsible amount of due-diligence in today’s development paradigm that often relies on utilizing the work of others based solely on public reputation. I could list two examples in recent months of how blind trust in popularity is no guarantee of security, but in short: like most seasoned development teams, we don’t have wool over our eyes. When one of our team members – our Director of Editorial Projects, Margaret Schneider – noticed a small disclaimer that ‘… guest accounts can access your directory via the Slack API’ while adding a single channel guest, a ticket went into the backlog to audit what information might be visible to the enterprising (if not nefarious) guest to our Slack workspace.
Any API-first hosted service has to strike a balance between racing to implement custom functionality for your in-house development projects and documenting, securing, and releasing functionality for public consumption. As a result, in-house projects often implement custom endpoints, permission sets, or undocumented features to ship a smooth user experience more quickly. Slack is no exception here. In late August, I grabbed the ticket from the backlog and knocked on some API doors to see what single channel guests could see. It turns out: a lot.
Slack’s API uses tokens. There are a lot of them, and most of them serve different use-cases and functions. Without going too far down the rabbit hole of recreating Slack’s API docs, suffice it to say that the most obvious avenue of exploit would be via a documented token type. However, this avenue wasn’t interesting (or exploitable) to us because Slack can be configured to require approval before creating these tokens. P.S.: If your workspace owners or administrators haven’t enabled this feature yet, they probably should. To Slack’s credit, our single-channel guest account wasn’t able to create a functional token that bypassed the workspace admin approval process. So next, we turned to the Marcus Junius Brutus of Slack: its in-house web client.
Using Slack’s web client is as easy as navigating to [Your Slack Workspace URL].slack.com and authenticating. From there, the web client negotiates and receives an `xoxs` token (via the OAuth dance) that is lightly documented (only a mention or two as a deprecated token type). The `xoxs` access_token is somewhat infamous among Slack hackers (be they earnest Linux users trying to authenticate alternate clients or security/pen testers disclosing severe Slack exploits): because they’re requested by the Slack web client, they require no workspace admin approval. How easy was it to grab and re-use this `xoxs` token? All we had to do was turn to the lowest-hanging fruit – the Firefox Web Developer Tools – to see.
Despite a few bugs and oddities, Firefox’s web development tools have some really convenient options for analysing browser XHR interactions. After logging in to the web client, pulling up the Developer Tools ‘Network’ Tab and filtering by XHR, we were ready for a brief peek under the hood.
It wasn’t hard to spot the 20 or so requests to various Slack API endpoints… a click over to the ‘Params’ tab showed that the POST payload for these API requests included our OAuth session’s `xoxs` token (appropriately named ‘token’). The ‘Response’ tab showed a standard JSON payload in response to most of these well-formed and documented endpoints. Now it was time to use the Firefox right-click ‘Edit and Resend’ functionality to replay the API request – but to a different endpoint: users.list.
It turns out that all that was required to completely iterate the full list of all Slack users for the entire workspace (including users of other channels, bot users, owners, admins, deleted users, etc.) is to just re-POST our payload to the users.list endpoint. Assuming you’ve tweaked your devtools.netmonitor.responseBodyLimit setting to a sufficient size in Firefox, you’ll be treated to a JSON payload that contains profile information for the entire Slack Workspace, all from the comfort of your browser. If workspace users were enterprising enough to populate their Slack profiles, you’ll have a list of names, emails, avatars, statuses, titles, phone numbers, Skype usernames and more. You can then subsequently call users.profile.get and iterate even more information like custom profile fields.
Why is this significant? Besides being a juicy target for pentesting, social engineering, or data-monetization, this level of information is generally presented in the Slack documentation and web-client functionality as off-limits to lowly single-channel ‘restricted’ guest tokens. Many companies may naively believe that inviting restricted guests to their Slack workspace will have no significant privacy implications (particularly to custom-fields). Or perhaps, like Alley, they may be concerned about the implications of non-disclosure agreements that forbid the publicized nature of a working arrangement. Many online collaborations (like web-based courses offered by one of Alley’s favs: Wes Bos) offer Slack communities: communities that are ripe for data-mining from ne’er-do-wells.
So why make this public? On August 29th, 2018, our Chief Software Architect, Matt Boynes, interfaced with Slack support to discuss our concerns. We were initially told
While we do highlight how a guest could access the API, those members would need to go through the trouble of attempting to create an app, with a token they may not be able to retrieve, to see the team directory….
After noticing and mentioning the token used in XHR requests from the slack client, though, Slack admitted the directory listing was exploitable without an explicitly approved token and a fix was imminent. Those conversations began over 100 days ago, and despite numerous status updates, promises that the fix is in QA, testing final changes, etc., a patch has not arrived. Our intent aligns with those of all responsible public disclosures: to bring the issue into the light so that companies and teams affected by it can accurately judge their exposure and take any necessary measures for their protection. As such, it’s probably important to mention these mitigating factors:
- The user must have (or be able to exploit) a current Slack web-client session with your workspace; ie: they need to be invited as a single-channel or multi-channel guest and log in to the web client
- The user must have some basic knowledge of how to recover a web-client issued Slack token
- The user must have some knowledge of what APIs they would like to query/work against
Keep in mind that when inviting a guest of this type, Slack appropriately discloses ‘guest accounts can access your directory via the Slack API’… but we ‘value working software over comprehensive documentation’, and the above list of mitigations is not a satisfactory privacy hurdle.
It’s our hope that Slack is true to their word: that they have a patch in testing to limit the scope of web-client tokens to users.list (among other endpoints like users.profile.get 😬) to return only the information that the guest ‘should’ see. We’ll continue to update this post with any relevant updates to these findings.
Disclaimer: Alley received no compensation for the timing or content of this disclosure and did not officially participate in their bounty program (props to them, by the way, for having one). We did alert Slack that we would disclose this, and Slack in turn asked us (nicely) to delay our disclosure until they fixed the issue. We were happy to do so under the sole condition that they could guarantee a resolution date in 2018, and unfortunately Slack was unable to provide that.