Share this…

Last year we switched to using Slack for all our internal communication and it’s working out nicely. It’s very developer centric in that it offers integrations with lots of services like Travis CI, GitHub, etc.

When we started using Slack one of our developers was sending a file, had his Developer console open and noticed that even though he’d not chosen to share the file public, the API gave back a public URL anyway. Much to his dismay when he tried it out in a new private browsing window he could download his file without authentication!

Everything you share on Slack automatically becomes available on a public url.

Concerned with the security of our communications (we don’t share financials or credentials through Slack fortunately, but we may share company or customer sensitive information) I decided to look into it and make it a teachable moment on ‘secret URLs’.


I shot some video at a bachelor party that I shared to the participants, a lot of whom I didn’t particularly know, as YouTubes ‘Unlisted videos’. All they needed was the URL and they could watch the videos, but that URL was not published anywhere.

And I shared a PDF with some work to a mailinglist via Dropboxes ‘Share Link’ functionality.
All they needed for access was the URL.

As a user I love secret URLs. They give me some wiggle room between “completely private / protected” and “completely public”. Some content isn’t exactly secret but doesn’t deserve to be published and communicated to the world at large. They bear some resemblance to the physical act of handing out a document in a meeting or photos at a party. The only way to gain access is to be granted so by someone who has been granted access before, forming a trust chain.
However, unlike in the ‘real world’, what is shared is not something physical, but information. And information wants to be free. It is (in some cases frighteningly) easy and fast to share information these days.

Intended or unintended (think of browser plugins, HTTP caching proxies or well intentioned friends and colleagues) these secret URLs can become not so secret.
And even if the link no longer works, that doesn’t mean the content has not been copied.

Now if you were to implement secret urls, your second line of defence would be toempower the user by giving her the ability to revoke a public link, explicitly in the UI (as Slack appears to do, more on that later) and / or implicitly by letting the user choose how long to share the link for.

But the first line of defence is to let the user choose whether or not something should actually be public. And users should be informed that secret URLs should only be used for semi-public information.



Putting aside the likelihood that a URL is leaked through some means, having public URLs opens up the possibility for brute forcing. Now the question “How easy is it to brute force a secret URL?” is actually quite interesting and the answer has changed a bit recently.

Brute forcing dep

ends on trying many combinations as fast as possible and is usually done on a local data set. This is mostly because of latency.


Going over a network is simply several orders of magnitude slower than trying a combination locally. The further you are from the target data, the more time is required to try a combination.

Fortunately for the brute forcer the advent of ‘the cloud’ has ensured two things:

  1. Quickly and easily have an army of machines with public IPs available, only paying for the short amount of time you will use them.
  2. Co-locating with your target in the cloud is much easier.
    Okay, maybe not in the same virtual or physical machine so you’ll still have a network hop, but it will be much faster.
    If you look at the latency infographic, this means going from milliseconds to microseconds.

A wholly unscientific benchmarking of default 404s against a small Digital Ocean LAMP VM produces the following ‘Request per second’ (rps) numbers:

  • laptop from office over internet, same country ~ 15 rps
  • small vm in same ‘region’ ~ 1400 rps
  • on actual destination server (no network) ~ 4000 rps

Brute forcing over the network does introduce some additional bottlenecks:

  • network throughput (fortunately most HTTP HEAD responses or 404 pages are tiny)
  • number of webserver processes available

It also introduces the possibility to be detected or stopped by generating too much load on the target.
Ironically the better tuned and configured the target is, the easier it is to brute force them.

At 1400 rps a space of 16 million combinations (6 character hexadecimal) is checked in 3 hours.
At 15 rps it will take 12 days.


8, same as passwords, duh.

If only it was so easy. Deciding how many characters you need depends on:

  • How many Requests Per Second will your servers will be able to take?
    The more hardware you have, the more optimisations, the more an attacker could try.
  • How soon you can respond?
    A dedicated file sharing service would be wise to have a security team that monitors traffic actively, a product that gets sold and run at a remote site may seldom to never have it’s logs analysed.
  • Which characters do you use?
    The more possibilities the better, but not all characters are as easily used on the web. Decimals or hexadecimals are a sure bet (though leading zeros may be problematic) but including the full alphabet adds 26 more possibilities, in both upper and lower case this adds up to 52. Adding special characters (like {}[]\| etc.) may be more trouble than it’s worth.
    Personally I’d go for [A-Za-z0-9], this gives 62 possibilities.
  • How many files you plan to make public?
    Often forgotten, but if you plan on making a significant amount of files public then again this helps attackers that may only need / want 1 of those files.

Here is a gist with JavaScript code that can help you decide: brute-force-chances.js.

However, as a default I would state that:

A token of 8 alphanumeric upper and lower case truly cryptographically random characters should be able to withstand Twitter like brute forcing load for a year.

Note that they must be cryptographically random (for instance by using the excellent Zend\Math\Random library in PHP) any ordering (like with using timestamps) would defeat the purpose.


Well… brute focing in theory then. Understandably Slack forbids automated testing:

Please only test with your own team when investigating bugs. Automated testing is not permitted.
– https://slack.com/whitehat

However with what we’ve seen and the JavaScript gist should be enough to prove the feasibility.

First let’s upload a file. In my own Slack Team (relaxnow.slack.com) I uploaded a file which made the Slack client do a request to /files.info that gives back some JSON data including the following:

     // ...
    "url":"<a href="https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/8cx20s4_-_imgur.jpg">https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/8cx20s4_-_i...</a>",
    "url_download":"<a href="https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/download/8cx20s4_-_imgur.jpg">https://slack-files.com/files-pub/T02EMLM07-F02GJ6FPC-a9d3f2/download/8c...</a>",
    "url_private":"<a href="https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/8cx20s4_-_imgur.jpg">https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/8cx20s4_-_imgur.jpg</a>",
    "url_private_download":"<a href="https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/download/8cx20s4_-_imgur.jpg">https://files.slack.com/files-pri/T02EMLM07-F02GJ6FPC/download/8cx20s4_-...</a>",
    "thumb_64":"<a href="https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_imgur_64.png">https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_i...</a>",
    "thumb_80":"<a href="https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_imgur_80.png">https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_i...</a>",
    "thumb_360":"<a href="https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_imgur_360.png">https://slack-files.com/files-tmb/T02EMLM07-F02GJ6FPC-ae2f96/8cx20s4_-_i...</a>",
    "permalink":"<a href="https://relaxnow.slack.com/files/boy/F02GJ6FPC/8cx20s4_-_imgur.jpg">https://relaxnow.slack.com/files/boy/F02GJ6FPC/8cx20s4_-_imgur.jpg</a>",
    "permalink_public":"<a href="https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2">https://slack-files.com/T02EMLM07-F02GJ6FPC-a9d3f2</a>",
    // ...

First off we clearly see is_public: false, but a (working) URL for permalink_public.

Next we see a lot of locations that the file (or copies of it) are hosted on. And most of them actually include the filename, which is against OWASP recommendations and potentially introduces vulnerabilities.
Then again, it does help against brute force attacks, which are made significantly harder when in combination with a random file id.

However when we look at the public links we see a public permalink without file name in the following form:


Trying out a couple of uploads in quick succession (which Slack makes very easy with drag and drop) gives the following public permalinks:


Which looks to be in the following format:

  1. Team ID: T0<7 character base 36 number>
  2. File ID: F02<6 character base 36 number>
  3. Token: 6 character hexadecimal (hash?).

Seems secure enough on the surface, but let’s look closer.



Very helpfully Slack includes the Team ID in the HTML output if you go to the log in page for a team:

var no_sso = false;

var team_id = 'T02EMLM07';

var email_regex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", 'i');
One down, two to go.



Fortunately for an attacker Slack helped by giving a different 404 when a file exists for the Team but the Token is invalid:

Access to this file has expired.

– https://slack-files.com/T02EMLM07-F02GJ6FPC-aaaaaa

Than it does when a File ID is invalid:

The requested file could not be found.

– https://slack-files.com/T02EMLM07-F02AAAAAA-a9d3f2

Next let’s look at the base36 numbers of the files uploaded in quick succession:


Or converted to base 10 and minus 990,000,000:


It seems that the numbers are sequential, perhaps for two file servers. Testing with another Slack instance reveals that this number is not bound to a Team. Likely this is the number of files uploaded.

While 36 to the 6th is still a significantly high number (35 days at 700 RPS), in 2014 already 55% could be discarded because it is sequential (< 17 days at 700 RPS for a single file).

However you can discard far more if you’re only interested in recent files.

Slack is very helpful here as it returns the time that the file was uploaded in the If-Modified-Since allowing you to use public links to correlate File IDs to a time.



The token was a 6 character hexadecimal, most likely a hash of some data. I could not find a correlation to file name or some other piece of data that an attacker might know.

When ‘revoking’ a public file in the UI, this token gets renewed.

However a > 50% chance of guessing this token can be achieved with 700 RPS in less than 7 hours. If you have the time and go at a less noticable 70 RPS it will take approximately 2 days to get the same chance.


Slack had taken a weak approach to securing private files. Combining this with the decision to make all files public lead to a rather large attack area that could be exploited by a dedicated attacker.