The Ultimate Guide to Stored XSS Attacks
1 Star2 Stars3 Stars4 Stars5 Stars (26 votes, average: 3.96 out of 5)

The Ultimate Guide to Stored XSS Attacks

Stored XSS Attacks, Also Known as Persistent XSS Attacks, Are the Type With the Farthest Reach and Highest Potential Damage

Recently, we took a closer look at one of the items on the OWASP Top 10 Vulnerability List – cross-site scripting (XSS). In that post, we covered the basics of XSS attacks and performed a quick overview on each of the various types of XSS. Today, we’re going to continue our series on XSS and do a deep dive on one of those specific types of XSS attack – Stored XSS, also commonly referred to as Persistent XSS.

Stored XSS can end up being the most dangerous type of XSS attack because of the way they’re carried out. Which scenario results in the most overall damage – a) a bad guy targets every single person that visits an ATM via a card scanner that’s planted within the machine, or b) he instead sneaks up behind a single, particular person and watches as he enters their PIN. The former scenario would end up having a broader and more severe effect because the hacking device (the card scanner) is stored on the ATM, and thus every single person that accesses is potentially a victim.

Stored XSS works in a similar manner. The attack vector ends up being permanently stored (hence the name) on the website’s server, and anyone that accesses the page thus becomes susceptible to the affects of the malicious code that lives there. Persistent XSS attacks are therefore such a significant threat because they can have such a wide-ranging reach and do not require a social engineering phase (like Reflected XSS attacks do, which we’ll cover in our next installment of this series) to get users to take a specific action like clicking a link.

So, how exactly do Stored XSS attacks work? What are the consequences of a successful attack? What does a real-world attack scenario look like? And most importantly, how can you protect against them?

Let’s hash it out.

How Do Stored XSS Attacks Work?

Persistent cross-site scripting attacks are able to occur when sites or web applications allow user input but don’t properly sanitize or restrict the contents of it. This allows for malicious code to be entered as input, which is then stored on the server and displayed to unsuspecting site visitors.

For example, if a hacker was able to include a malicious script when posting a comment on a popular blog, every person who read that blog article would be exposed to the malicious script. The attacker’s code is incorrectly treated as valid input by the site in question and doesn’t get properly encoded as a result.

An example of a text input field that could potentially be vulnerable to a Stored XSS attack.

Now that the malicious code has a persistent presence on the target site, it will be executed every time a visitor accesses the page. The browser allows it because its same-origin policy is being circumvented – it would normally block the code but doesn’t in this case because it’s seemingly coming from a valid page. And to make matters worse, the stored nature of the script means that it subsequently gets served to every single user that triggers the script execution on the compromised page.

Text input fields are the most common place for the injection to occur, but locations that don’t normally contain scripts (like image tags or event attributes) are also prime targets. Any element that isn’t subject to input validation, encoding, or filtering can possibly be taken advantage of by an attacker.

Stored XSS is relatively easy for hackers to pull off because all they have to do after finding a vulnerable site is simply inject their evil code and sit back and wait for victims to pay it a visit (hackers will sometimes actively promote their handiwork via spam messages or social media posts, though). Locating the target site itself is the hardest part of the process. The kinds of vulnerabilities required don’t exactly grow on trees and preventing them is usually one of the top priorities for administrators of at-risk sites.

The process for finding a vulnerable target usually goes as follows:

  1. An attacker finds a website that may be vulnerable
  2. They test it by attempting to store a script on the server and exploit the vulnerability
  3. They navigate to the page that would deliver the malicious code
  4. They check to see if the script executes

This is most often a manual process, however automated tools do exist that are capable of automatically and remotely injecting scripts.

Not only does an attacker need to find a weakness that allows for permanent script embedding, but they also need to find a website with sufficient traffic in order to make it worth their while.

The Primary Targets of Stored XSS Attacks

Pretty much any site that allows for the sharing of content by users is a potential target for Persistent XSS attacks. Think anywhere that has comment fields or text boxes for user inputs, and any sites where that input is then stored and displayed to other users. Typical targets include:

  • Message boards
  • Social networking sites
  • Comment sections of websites such as blogs or video sharing platforms
  • Collaboration tools
  • CRM/ERP systems
  • Email server consoles

Stored XSS attacks succeed because of the user’s trust in genuine websites – the site just happens to have a vulnerability that can be exploited via XSS.

Consequences of Stored XSS Attacks

The impacts of Stored XSS attacks are wide ranging, and attackers can achieve a variety of goals by using this technique. The theft of session cookies and sensitive data are among the most common aims. By stealing session cookies, a hacker can perform session hijacking, allowing them to impersonate their victim within the site and potentially gain access to all kinds of private information.

Persistent XSS attacks can also be used to alter the appearance of a website, like a kind of digital graffiti. This can range from subtle changes that try and trick the user into carrying out an action, to full-blown defacement of a site via political statements or offensive images and words.

Attackers can use stored XSS attacks to redirect users to another site. Most likely they aren’t going to send you somewhere nice or merely Rickroll you (although that has happened before). Instead, you’ll be directed to a hostile site that most likely contains malware, harmful scripts, phishing attempts, or all the above. Phony login pages are a common attack vector as well, and will appear similar to the legit version but instead send your credentials straight to the attacker.

An example of a fake Facebook login prompt.

Another possibility is a keylogger being planted on a victim’s machine. All their keystrokes will be sent to the attacker, which could contain goodies such as credit card information and passwords.

Stored XSS Example

Now we’ll look at how a Stored XSS attack would actually be carried out in the real world. We’ll use an ecommerce site as an example – let’s call it Wanda’s Widgets. A common element on product pages is a place for customers to leave a review. Wanda installed a WordPress plugin on her site that allows users to rate her products on a scale of one to five and also leave a text review if they wish. Unfortunately, though, Wanda’s plugin contains a vulnerability that lets third parties embed HTML tags within their review text.

An attacker takes advantage by submitting the following review:

I love this product, and it’s a bargain too! “<script src=””> </script>”

Which means that the embedded tags they just submitted as part of their review are now part of the product page, no matter who opens it. So when Bob navigates to the page, “passwordstealer.js” will be executed in his browser. That’s bad news for Bob because his session cookie just got stolen. The hacker is now impersonating Bob on the website and has access to his credit card details. Bob has no idea though, and he might not even figure out what happened until after he gets his next credit card statement.

How to Prevent Stored XSS Attacks

The biggest thing is to never allow raw user input. You should treat all user input as untrusted and suspect. It’s critical that all user input be strictly filtered and properly validated. Likewise, any data that’s being output should be encoded. This keeps it from being treated as active content.

Other preventative measures include:

  • Web Application Firewalls (WAF) – they employ signature-based filtering to stop malicious requests from being fulfilled.
  • Content Security Policy (CSP) – CSP can be enabled on your web server, and helps to detect and stop attacks.
  • Use vulnerability scanners to locate XSS vulnerabilities on your site. These work by testing every single entry point (places where users can input data to be processed by the site) and exit point (places where responses appear).
  • Technically, you can disable JavaScript in your browser, but this isn’t practical since it would stop most websites from functioning correctly.
  • Employ whitelisting to only allow specific characters or patterns as input. Whitelisting is preferrable to blacklisting since blacklisting requires constant updating, and the sheer quantity of entries is difficult to manage.

A Persistent Danger

Stored XSS attacks are an imposing threat, especially from the end-user’s point of view due to the fact that there’s not many preventative measures that can be taken. The danger is compounded by the nature of Stored XSS attacks, it’s even in the name – they stick around and affect everyone that comes in contact with it.

It’s up to site owners to make sure their input fields are safe so that their users don’t get exposed to hidden surprises when they least suspect it. All it takes is for a few key preventative measures to be enacted on the server side of things to effectively reduce the risk of Persistent XSS attacks to nearly zero. Otherwise, users will be crossing their fingers and playing a game of Russian Roulette every time they browse your site. 

Protect Your Site With CodeGuard Backup

CodeGuard Logo

It’s like an undo button to reverse damage done by a mistake, cyber attack, a bad update, or other issues.

1 comment
  • The recommendations here do not really help a web developer understand the underlying problem. The “persistence” in a web site can be a lot of things, but in the main it is a relational database (MySQL, Postgre, MS SQL Server, Oracle, etc.) or a JSON document-based database where each record is a key (often a GUID/IUID) and the value is a serialized JSON representation of an object model. The approach to preventing Persistent XSS depends on the persistence. For RDBMS, the developer needs to understand that the database query will result in a byte array from the database formatted according to a standard called Tabular Data Stream (the “other TDS”). Microsoft and Sybase follow the original TDS standard. Other vendors like Oracle add things to it.

    In Java there is the ResultSet interface. In .NET it is the DataReader class (abstract, extended by classes like SqlDataReader, OracleDataReader, etc.). Both basically provide an API into the TDS byte array. Both have functions often called “getters” (getString(), getBool(), getInt(), etc. The problem is these getters, “out of the box”, do not do any validation as they read from the TDS byte array and return the value into the application.

    What happens here is Static Code Analysis tools often point the developer to where they will write the data to the web page. This isn’t, though, where the real problem lies. It is in the ResultSet or DataReader where values from the TDS byte array are allowed into the app without validation… So how to fix?

    To use Java as an example: Encapsulate ResultSet with a SecureResultSet class. Make sure to have all the same getters with identical argument signatures. The constructor for the SecureResultSet class should take a ResultSet instance and a hashtable instance. Set these to field variables. The hashtable should use the column name as the name and a regex pattern for the column as a value.

    In the encapsulated getString() (as an example) use the regular ResultSet getString() to get the string value from the column. Then do a RegEx check by resolving the correct pattern from the hashtable by the column name. (You can use the column index too, but make sure it is the name for the name/value pair.) If the RegEx pattern matches, return the string. If not ***THROW AN EXCEPTION***. I highlight this because most SCA tools like Fortify will still hit on the vulnerability unless you interrupt the code execution with an exception. Then you can do a find/replace for “new ResultSet(” and replace with “new SecureResultset(“.

    If you are using a NoSQL implementation like MongoDB or RavenDB (for .NET), things are much easier and cleaner. Use JSON Schema and set the “pattern” property for each attribute with an applicable RegEx. That’s it. Done. (Can you tell I like NoSQL?) Unlike the TDS byte array returned by a database query, here the function that “deserializes” the JSON text into the model enforces the RegEx pattern before completing the instance of the model. Same basic idea; validate as early as possible as the persisted data enters the app.

    Lastly a word on encoding. Just because you successfully VALIDATE at the top of the stack (when the getter() reads from the TDS byte array into the application) does not mean you do not need to ENCODE and the bottom of the stack (where the values are written to the HTML response before sending to the browswer. VALIDATE at the TOP and ENCODE at the BOTTOM of the stack and you have really secure code against Persistent XSS.

Leave a Reply

Your email address will not be published. We will only use your email address to respond to your comment and/or notify you of responses. Required fields are marked *

Captcha *


Mark Vojtko

After starting his career as an engineer, Mark pivoted to tech marketing, which combines his love of technology and analytical thinking with a generous dose of creativity. In addition to contributing to Hashed Out, Mark is The SSL Store's Product Marketing Manager.