Always Stay Online and Active on Slack with WebSocket Framejacking

Slack: Always Stay Active

Goal: Find a simple way to always stay active on Slack, even if the browser is minimized or out of focus, without giving away your password to an online service or needing an API key.

Even though Slack is on a dedicated monitor beside you, if you’re not focused on it you’ll appear “away” and “not at your computer”. Do you subconsciously think team members are sleeping when they appear away at 10 am? Let’s always be active until we close the Slack tab or manually set ourselves as away. This is fair: Slack is open somewhere, so we are active.

Is Eric really away?Eric is usually activeAm I really away or offline when I’m in a different window?

By changing just four characters in Slack, we can always stay active without going inactive, even after hours and hours with a minimized browser.


Are There Existing Solutions?

Let’s see what kinds of solutions exist or we can think of before we analyze an extremely simple and effective solution.

Online service that pretends to be you

One solution requires giving away your user name and password to an online service that pretends to be you just enough to convince Slack you are active. Who is to know how safe this is? This doesn’t work for Enterprise Slack that uses SSO authentication.

Slack on an always-open smartphone

Another solution is to open Slack in the foreground on your phone and never let your phone sleep. You’ll need a phone and charger.

Desktop Slack app with a mouse jiggler

Yet another solution is to open the Slack desktop app and use a mouse jiggler to prove to Slack you are active and awake. You’d need hardware or some way to nudge your mouse now and then, plus the desktop app.

Slack API to perform user actions

Others have even written Python, Java, NodeJS, etc. code to generate faux user activity via the Slack API with a personal API key. This seems like a neat idea, but the API changes now and then, and it is unclear how to how to change the user status. We like don’t have Enterprise Slack access to the API.

Self-refreshing frame

You might consider writing a self-refreshing iframe that just loads Slack over and over again. Slack sets the X-Frame-Options header to sameorigin, so Slack will not even load in an iframe. Good thinking, though.

Selenium to click around in Slack

Another interesting solution is to fire up a web-testing framework like Selenium to load a headless browser that “clicks” around Slack now and again. Some steps are needed to enter your credentials (and hope the UI doesn’t change and a Captcha doesn’t appear), plus the programming knowledge required is high. This likely doesn’t work with Enterprise Slack and SSO authentication.


Let’s Arrive at a Neat Solution Together

As a rewarding read, let’s arrive at this solution together by asking ourselves some questions.

Question: How does Slack know we are away? Does the browser signal inactivity or does the Slack server keep track?

Let’s open Chrome DevTools and watch the console, XHR network requests, and WebSocket frames.

Sample WebSocket frames
WebSocket messages
Console shows Slack is aware of window blur and focus
Console messages

We see that when the browser loses focus and then is made active again, some console messages appear, but there are no similar WebSocket messages.

Question: Can we remove browser event listeners related to losing focus?

Let’s try that. We can easily remove all blur event listeners using DevTools like so.

Window event listeners
Window event listeners

When we focus on another window or minimize the Slack window, no blur events fire. So far, so good. Now we wait for thirty minutes to see if we go inactive.

Some time goes by …

Darn. We went inactive. Let’s see what network requests took place.

No unusual XHR requests

There were no unusual XHR requests, but there are some interesting WebSocket frames.

WebSocket messages when user goes away

Did you notice these messages are response frames? We’ve now learned the server told us that we went inactive, but no browser event effectuated it.

Let see if there are any interesting messages when we return to an inactive Slack window.

Messages when the user becomes active again

The server again told us we became active when we entered the browser window.

Question: What user actions signal to the server we are active?

Let’s click around the Slack channels and type some messages and observe the WebSocket.

Activation WebSocket messages

We see a common typing message when we initiate typing. We also see an uncommon tickle message occurring when the browser goes out of focus then back in focus followed by a click. These are the only two innocuous events to signal activity.

Question: Can we replay these activity messages?

No. There is a sequential id in the sent message frames. We cannot replay one of these messages under a JavaScript setInterval(), for example.

Question: Can we predict the next id in the sequence for spoofing?

We could be clever and try to keep track of the current sequence id, then replay a doctored activity message with an id++. We’d likely invalidate the next message with the genuine id, or even enter a race condition with an asynchronous invocation in the Slack web Service Worker.

Here is the logic used to keep track of the sequence ids if you are curious.

Socket message sequencing
Socket message sequencing

It’s unclear if the sequence id is synchronized across new WebSockets, or if the sequences are reset or otherwise independent. Besides, Slack can modify its implementation at any time, say to send even-numbered sequence ids instead. Let’s not be over-clever with the sequence ids and move forward on the assumption we cannot predict the sequence.

Question: Can we framejack ordinary, non-critical messages and modify them?

Let’s look at some background messages and see what candidates there are for framejacking (my term for hijacking a WebSocket frame). A WebSocket, being a stream, may have a message fitting in a single frame, which is the case for most Slack messages. Let’s look.

Common WebSocket messages
Common WebSocket messages

Ping! Every ten seconds or so a ping is sent to the server and a pong is received. A ping message has the same format as the tickle message.

Ping and tickle messages
Ping and tickle messages

Let’s see if we can spoof a tickle message by changing the four characters “ping” to “tickle” in framejacked ping messages every few minutes (more on that soon). Possibly simple and elegant.

Question: Can we proxy the WebSocket constructor and handlers to intercept messages?

Yes and no. Granted, it’s trivial to MITM WebSockets, known as intercepting, and make modifications. To save you a few hours, I’ll tell you that WebSockets close and open and are even killed over time, so effort needs to be invested to ensure we always framejack the correct socket.

Many WebSockets come and go
Many WebSockets come and go
Question: Should we run setInterval() for framejacking all sockets?

No. I’ll save you yet more time. Over the day, many sockets may be instantiated so many setInterval timers would be pending, and stacked internal timers can behave poorly, especially if variable references go out of scope in the meantime.

A wiser implementation is to not use timers as all and make use of timestamps whereby each incoming message kicks off some elapsed-time check to initiate the faux activity.

Question: Is there any downside to skipping pings?

Yes. WebSockets use a heartbeat protocol to know to keep the socket open on the server. Slack uses a ten-second ping-pong challenge with sequential messages, and the client-side JavaScript has a pong watchdog that will close the socket if one pong is missed.

Pong timeout check in Slack
Pong timeout check in Slack

Here, every ten seconds a check for a pong with the same id as the ping is performed. If a matching pong is not found within ninety seconds, the client closes the socket. There is more logic in the send() method involving a buffer, but one missing pong will hold up the check.

Question: How can we safely framejack a ping then?

Let’s not interfere with the Slack JavaScript or try to hack private variables. Should we prevent the socket from being closed? Here is an experiment I ran by editing the cache of a Slack JavaScript file.

Experiment with large ping timeout
Experiment with large ping timeout

What happened is no more pings were sent after one ping was framejacked and then Slack just stopped working a short while later.

Instead, let’s intercept the ping, massage it into a tickle, and “receive” a well-crafted pong into the WebSocket layer so the client-side JavaScript is satisfied.

Fake some pong messages from the console
Fake some pong messages from the console

It’s trivial to spoof a pong from the DevTools console, but we shouldn’t be satisfied sending a simple JSON string as a pong, though; let’s really forge a trusted EventMessage with all the trappings and attributes of a real message received on the wire. This makes it future-proof as well.

Trusted pong EventMessage
Trusted pong EventMessage

To save you yet more time, one cannot just forge a trusted EventMessage object (a real message received by WebSocket) and modify it; some engineering needs to be done to tamper with a read-only event message to return a valid pong, such as making the data attribute writeable.

Trusted EventMessage forgery
Trusted EventMessage forgery

It’s okay to admit that forging a trusted message is exciting. What we’ve done is saved a reference to a real pong (which we cannot easily clone), and made the data attribute writeable.

Question: Does this work with a minimized browser?

Yes. Slack uses a Service Worker that runs in the background in its own thread outside the rendering thread. Web Service Workers are responsible for sending alerts, updating charts, syncing data, and more.

Slack Service Worker
Slack Service Worker

Here is a console log of the script functioning well while the browser is minimized and hidden.

Console logs while browser is minimized
Console logs while browser is minimized

The Slack: Always Stay Active Script

We only need Chrome (or Brave) with a popular1 browser extension called Tampermonkey to inject some JavaScript just on Slack pages.

This started as a proof of concept but expanded to include visual feedback and integrity checks to indicate if Slack changed something pertinent to our workflow. You can even remotely stop the script on all your browsers on different machines. You can still set yourself away manually as well. The script is laid out so it is easy to follow and verify its safety. Enjoy.

Quick Start

A thin orange line appears while Slack boots, then it becomes green when active.

Slack: Always Stay Active is running
Slack: Always Stay Active is running
Manually setting yourself away works
Manually setting yourself away works

If it goes red, internal sanity checks have failed so Slack has changed something major. This is designed to be future-proof, but if Slack changes tickle to, say, caress or nudge, then that is a breaking change to which we can still adapt.

Advanced Features

Send yourself these messages to control the script across all of your open Slack windows.

  1. sigkill! – close all your open Slack windows.
  2. sighup! – refresh all your open Slack windows.

Console Commands

You can test out the script from the DevTools console with the following invocations:

  1. slackasa.ticklenow() – Framejack the next pong to signal activity now.
  2. slackasa.listsockets() – List all the open sockets and timestamps.
  3. slackasa.reload() – Reload the page.

Installation Steps

Important! (Update Sept 25, 2022) Use a web browser that uses Manifest V2 like FireFox or Chrome before 2023. Chrome, Brave, Edge, and all Chromium-based browsers (most browsers) are moving to declarative network requests which block user scripts and cripple adblockers (Google makes money from ads).

Step 1. Add Tampermonkey to Chrome.

Get Tampermonkey for Chrome or Brave
Get Tampermonkey for Chrome or Brave

Step 2. Create a new empty userscript without saving.

Create a new user script

Step 3. Copy and paste the script at the bottom of the page. Save. Close Tampermonkey.

Paste the script to Tampermonkey
Paste the script to Tampermonkey

Step 4. Visit Slack. Find the Tampermonkey icon, and enable Tempermonkey and the ‘Slack: Always Stay Active’ userscript. Reload Slack.

Activate the Slack: Always Stay Active script
Activate the Slack: Always Stay Active script
Slack: Always Stay Active is running
Slack: Always Stay Active is running

Discussion

This started out as a mental exercise in finding a way to keep Slack active. Personally, I’m on web Slack all the time with it open on several computers and my phone, so at least once every thirty minutes I cause activity.

However, sometimes I’m engrossed in my IDE for long stretches and even though Slack is on a dedicated monitor beside me, I look “away” and “not at my computer” – far from it. Does this happen to you too?

Success: We’ve found a safe, viable, and non-intrusive solution to always stay active in Slack and Enterprise Slack, unless we close all Slack browser windows, with a tiny adjustment to one word in a well-timed WebSocket frame.

Full Script

This was written and developed in vanilla JavaScript in the Tampermonkey script editor in my spare time over the weekend. Encapsulation isn’t perfect as it’s not transpiled from TypeScript. The goal is to make it easy to read and understand. Why not publish this script? To require users to examine the code and verify it is safe as a habit. If you share this script, kindly attribute it. Thank you.

Copy script to clipboard

Notes:

  1. There are over 10 million users of Tampermonkey.