This blog post is a cooperation between Chip (lead Semaphor UI engineer) and Reed (QA tester) in finding some vulnerabilities in Semaphor's URL and profile handling. We're going to bounce back and forth between different observations, and we'll indicate a perspective change by prefixing each section with the speaker's name.

Reed: As a member of the QA team at SpiderOak my main duty is to find and report bugs found in our applications. I've spent a good amount of time poking at our Semaphor chat application.

Chip: The Semaphor front-end is built in HTML/JS using React, and packaged using Electron and Cordova. We target Windows, OSX, Linux, Android, and iOS through a single code base with some glue for specific platform needs.

Reed: Simply said, there are a lot of places to poke, prod, and cause general mayhem across the application.

Part of the Semaphor profile page allows a user to upload an image to use as an Avatar. Current restrictions keep users from uploading anything other than a few different image formats: .png, .jpg, etc. A user can select to upload a gif, however the avatar will only display the first frame and the avatar will not be animated.

Chip: That's because the upload process automatically resizes any input image, squashing the animation in the process.

Reed: One exception to this rule was an engineer named Frank. Frank's avatar is an animated Clippy doing his Clippy dance. It was a point of mild frustration that I couldn't figure out how he managed to upload an animated gif as his avatar. But after some poking...

As we can see, there are some workarounds to the restrictions. I opened up the local account database and found that user profile information is stored as a JSON blob. The image itself is converted into base64 on the user side before being uploaded to the server. We can gif-i-fy our avatar by converting a .gif into base64 and manually pasting the resulting code into our local account database. After supplying the base64 encoded .gif all we need to do is launch Semaphor and save the profile settings which will upload the new avatar to the server.

Frank and I credit ourselves with causing at least ten or so hours of productivity loss due to the company changing their avatars from static images to gifs once this was figured out.

Chip: I did an animated avatar too, but as the UI engineer I was able to just cut out the image processing code on a custom build. :)

Reed: One more side effect was my interest in finding other ways to manipulate the user profile for nefarious ends. An older issue with how URL's are parsed and displayed seemed like a good place to start.

The '@' symbol is used to trigger a mention alert to another user in the same channel and it seems to be causing some issues in how hyperlinks are displayed (Chip: It's actually an unrelated URL parsing issue with punctuation). After bringing this topic up for discussion with Chip, he returns with some fun news that apostrophes are not escaped in the click handler.

Chip: The ugly thing about using Markdown formatting is that Markdown parsing libraries typically don't output directly to your widget toolkit. Almost universally, they convert Markdown to plain HTML, which you must then integrate into the DOM in some way. With React, you paste the output into a <div dangerouslySetInnerHTML={ ... }/>, which says "dangerously" for a reason. If the HTML you're inserting is user controlled and your filtering is inadequate, you open the possibility for code execution.

That's unfortunately exactly what happened here. We rewrote the link handling rules in the markdown-it parser to override the default link behavior and open the link in the system URL handler. Because the generated HTML was divorced from the rest of the application, the call was in an inline onClick handler and passed in the URL as a single-quoted string. We didn't think apostrophes were valid in URLs because of the broken punctuation behavior we found above, so we didn't escape them. Turns out that assumption was wrong, and you could execute code on click by simply pasting a url that looked like http://some.url/');alert('foo (getting a user to click on such a fishy URL is left as an exercise to the reader).

Reed: I was fascinated with this little discovery and ready to find my own RCE. I felt like user mentions would be a good place to play. When executing an @ mention to another user, their display name is parsed and re-displayed in a bubble denoting that the target user will receive a notification. A typical @ mention will look like this.

I attempted to add all sorts of weird stuff to the username but I wasn't having any luck. Between the OWASP XSS cheat sheet and perusing through w3schools HTML tags page, I started making some progress.

Well that looks promising. We've managed to mangle how the display name will be presented. Can we take it a step further?

Nice! We just found a second major issue. A user can craft a display name such that when another user attempts to send them a notification, code will be executed on the sender's machine! Fortunately there is a character limit on the display name and it would be hard to do too much damage. Both issues were patched in a hot-fix released a couple days after the initial discovery.

As coincidental as it sounds, we discovered this issue right around the same time that Iván Ariel Barrera Oro (@HacKanCuBa), Alfredo Ortega (@ortegaalfredo) and Juliano Rizzo (@julianor) discovered a similar RCE in the Signal web application.

QA is much more than following a script of tests. Curiosity might have killed the cat, but it fuels us chaos monkeys.

Chip: And it makes all our software better. :)