The original post can be read here.
Subscribe to AppSec Monkey now and get the 2021 Web Application Security Checklist Spreadsheet as a welcome gift for FREE!
It's scary out there for developers! One mistake in the code, one vulnerability in a dependency, one compromised developer workstation, and your database is in Pastebin and you're on the news.
So where to look for guidance? OWASP's top 10 list is just too short and focuses more on listing vulnerabilities than defenses. Whereas the ASVS list is rather cryptic and vague for practical purposes.
This article is an attempt at the golden mean. We'll go through some practical steps that you can take to secure your web application from all angles. Let's begin!
Defending Threats On The Browser Side
- Use HTTPS and only HTTPS to protect your users from network attacks
- Use HSTS and preloading to protect your users from SSL stripping attacks
- Serve cookies with the 'Secure' attribute to protect your user from network attacks
- Generate HTML safely to avoid XSS vulnerabilities
- Sanitize and sandbox untrusted content to avoid XSS vulnerabilities
- Implement an effective Content Security Policy to protect your users from XSS and other vulnerabilities
- Serve cookies with the HttpOnly attribute to protect them from XSS attacks
- Serve downloads with a proper Content-Disposition header to avoid XSS vulnerabilities
- Serve API responses with a proper Content-Disposition header to avoid reflected download vulnerabilities
- Use your platform's anti-CSRF mechanism to avoid CSRF vulnerabilities
- Validate the OAuth/OIDC state parameter to avoid CSRF vulnerabilities
- Use HTTP verbs properly to avoid CSRF vulnerabilities
- Serve cookies with the SameSite attribute to protect your users from CSRF vulnerabilities and optionally defend XSS as well
- Create a fresh session ID on login to protect against session fixation attacks
- Name your cookies right to protect against session fixation attacks
- Serve proper Cache-Control headers to protect your user's data from subsequent computer users
- Serve a Clear-Site-Data header upon log out to protect your user's data from subsequent computer users
- Log your users out properly to protect their data from subsequent computer users
- Don't transmit sensitive data in the URL because URLs are not designed to be secret
- Use a referrer policy to prevent URL addresses from leaking to other websites
- Use a unique domain name for your application to protect it from other applications under the same origin (and vice versa)
- Don't use CORS unless you have to, and if you have to, be careful with it
- Use WebSockets properly to avoid CSRF and other vulnerabilities
- Use U2F tokens or client certificates to protect your critical users from phishing attacks
Defending Threats On Server Side - Application
- Validate input properly to protect your application from... so many vulnerabilities
- Catch exceptions gracefully to avoid leaking technical details
- Don't do authentication yourself
- Authenticate everything to reduce the attack surface
- Use MFA in your application to break the trust relationship to the identity provider
- Use strict access controls to prevent unauthorized access to data or functionality
- Use proper tools and techniques to avoid injection vulnerabilities
- Construct database queries safely to avoid SQL injection vulnerabilities
- If you must run OS commands, do it right to avoid command injection and related vulnerabilities
- Avoid XML vulnerabilities by configuring your parsers properly
- Avoid URL injection vulnerabilities by using proper class for URL construction
- Avoid path traversal vulnerabilities by using a proper class to construct the paths
- Don't use the filesystem for untrusted content (e.g. uploads) if you can avoid it
- Don't execute dynamic code to avoid remote code execution vulnerabilities
- Use serialization carefully to avoid deserialization vulnerabilities
- Defending Threats On Server Side - Infrastructure
- Defending Threats On Server Side - Architecture
- Defending Threats On Server Side - Monitoring
- Defending Threats On Server Side - Incident Response
Secure Development Considerations
- Threat model
- Force peer review in source control
- Automate the CI pipeline and restrict mere mortal access to it
- Sign the build artifacts
- Run a static application security scanner as part of the CI pipeline
- Verify dependencies on build and keep them at a minimum
- Run a dependency security scanner as part of the CI pipeline
- Run a container image security scanner as part of the CI pipeline
- Automate deployments and validate signatures
- Have a security champion
There are a couple of threats on the end user's side that you as a developer can help mitigate. They include:
- Attacks through malicious websites/links in the user's browser.
- Attacks on the user's local network.
- Attacks where someone accesses a shared web browser before or after the user (for example if sensitive information is left in the browser cache then subsequent computer users will be able to retrieve it even if the previous user has logged out).
This one you probably already knew. Encrypt all connections between your user's web browser and your web server. Doesn't hurt to also disable some of the older cipher suites and protocols.
It is not enough to encrypt the "sensitive" portions of a website. A single unencrypted HTTP request anywhere under the domain can be intercepted by an attacker on the network, who can then forge a response from the server with malicious content in it.
Luckily HTTPS is easy these days, you can get both the certificate (LetsEncrypt) and tools for automatic certificate creation/management (CertBot) free of charge.
Strict-Transport-Security is a header that can be used to tell web browsers that from here on always use an encrypted connection (HTTPS) when connecting to this domain.
This will prevent so-called SSL stripping attacks where an attacker on the network intercepts the very first HTTP request made by a browser (which is often unencrypted), and forges a reply to that unencrypted HTTP request right away, pretending to be the server and downgrading the connection to intercepted plaintext HTTP from then on.
One caveat is that HSTS will only protect an application if the user has already successfully visited it before. To overcome this limitation you should submit your site to https://hstspreload.org so browser vendors can hard code your domain to the HSTS list.
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Be mindful when implementing HSTS. It will force encrypted traffic to your website and if you still have plain text your website could break. So start with a small
max-ageand ramp it up once you're confident that everything still functions properly. And leave preloading as the last step because it's painful to cancel.
Configure your cookies with the
Secure attribute. This will prevent them from being leaked over an (accidental or forced) unencrypted connection.
Set-Cookie: foo=bar; ...other options... Secure
To avoid XSS (Cross-Site Scripting) vulnerabilities, use one of the following:
- A template engine. If you have a traditional web application where HTML is generated and parameterized on the backend server, do not craft HTML through string concatenation. Instead use a template engine such as
Jinja2for Python, and so on.
If you use a template engine, ensure it's configured correctly to automatically encode parameters properly, and don't use any "insecure" functions that bypass the automatic encoding.
It's best to just avoid untrusted content. But sometimes you have to retrieve raw HTML from e.g. a remote source and then render it on your website. Or maybe you have to allow your users to write posts with a WYSIWYG editor. There are many use cases.
To avoid XSS (Cross-Site Scripting) vulnerabilities in these scenarios, sanitize the content first with
DOMPurify and then render it inside a sandboxed frame.
Even if your WYSIWYG library claims to remove evilness from the HTML, you can break this trust relationship "I trust my WYSIWYG library to sanitize the content" by re-purifying and sandboxing the content nevertheless. The more trust relationships you break the more secure your application gets.
Implement an effective Content Security Policy to protect your users from XSS and other vulnerabilities
A Content Security Policy (CSP) serves as excellent protection against XSS (Cross-Site Scripting) attacks. It also protects against clickjacking attacks among other things.
So make sure to use it! CSP by default prevents pretty much everything so the fewer things you put in it the better. For example, the following is a good policy to start with:
Content-Security-Policy: default-src 'self'; form-action 'self'; object-src 'none'
It allows loading scripts, styles, images, fonts, etc. from the web application's origin but nothing else. Most notably it will prevent inline scripts (
<script>...</script>), which makes exploiting XSS vulnerabilities difficult.
form-action: 'self' directive prevents creating malicious HTML forms on the website (think "Your session has expired please enter your password here") and submitting them to the attacker's server.
Whatever you do, do not specify script-src: unsafe-inline because then your CSP will lose its mojo.
And finally, if you have concerns about CSP breaking something in production you can first deploy in
Content-Security-Policy-Report-Only: default-src 'self'; form-action 'self'
To generate a CSP policy for your website, use the CSP tool
Configure your cookies with the
Set-Cookie: foo=bar; ...other options... HttpOnly
To avoid XSS (Cross-Site Scripting) vulnerabilities when serving downloads to your users, send them with a Content-Disposition header that indicates an attachment. This way the file won't render in the end user's browser directly, which could result in an XSS vulnerability in the case of e.g. HTML or SVG files.
Content-Disposition: attachment; filename="document.pdf"
If you want some specific files to open in the browser (like perhaps PDF documents for usability reasons), and you know that it's safe to do so, you can omit the header or change
inline for that particular file extension/extensions.
Serve API responses with a proper Content-Disposition header to avoid reflected download vulnerabilities
There is an attack called reflected file download (RFD) which works by crafting an URL that downloads as a malicious file extension from your API, reflecting a malicious payload inside it.
You can prevent this attack by returning a
Content-Disposition header with a safe
filename in your API HTTP responses.
Content-Disposition: attachment; filename="api.json"
To protect against Cross-Site Request Forgery (CSRF) vulnerabilities, ensure that your platform's anti-CSRF mechanism is enabled and working as intended.
There is a kind of CSRF attack related to OAuth/OIDC where the attacker unwittingly logs the user in with the attacker's account. If you are using OAuth/OIDC, avoid this by ensuring that you are using a properly configured and reliable software library for handling the authentication flow so that the
state parameter gets validated.
Never use anything except for
DELETE for making any changes.
GET requests for example are usually not covered by anti-CSRF mechanisms.
Serve cookies with the SameSite attribute to protect your users from CSRF vulnerabilities and optionally defend XSS as well
Configure your cookies with the
SameSite attribute. This will prevent most CSRF (Cross-Site Request Forgery) vulnerabilities, where a malicious website submits e.g. a form on behalf of your unwitting user, from being successfully exploited by an attacker. There are two modes,
Lax mode (with the exception of Lax+Post mitigation, see link below) is just fine for preventing most CSRF attacks, except GET-based CSRF vulnerabilities where you make the mistake of making changes (e.g. modifying some database record) in a GET request handler. The
Strict mode prevents that sort of blunders from being exploited as well.
Strict mode has another powerful side effect, it makes reflected XSS (Cross-Site Scripting) vulnerabilities practically impossible to exploit as well.
With that said, the
Strict mode is not well suited for most applications because it breaks authenticated links, that is, if your user is logged in and opens a link on another website to the application then in the tab/window that opens the user will not be logged in (because the session cookie was not sent along with the request due to the strict mode).
But at least implement
Lax mode, there's no harm in doing that and it serves as a fantastic safeguard in case a CSRF vulnerability creeps into your codebase.
Set-Cookie: foo=bar; ...other options... SameSite=Lax
Set-Cookie: foo=bar; ...other options... SameSite=Strict
- An attacker sneaks a cookie, say,
JSESSIONID=ABC123into your user's browser. There are many ways the attacker can go about this.
- Your user logs in with their credentials, submitting the attacker's chosen
JSESSIONID=ABC123cookie in the login request.
- Your application authenticates the cookie, and the user will be logged on from that point onwards.
- The attacker who also has the cookie, is also logged on as the user from that point onwards.
So to prevent this, create a fresh, authenticated session ID and return that to the user, instead of authenticating the existing cookie which might have been compromised.
This is not very widely known, but when it comes to cookies, name matters! Name your cookies
__Host-Something and web browsers will...
- Not allow for the cookie to be set over an unencrypted connection which protects against session fixation attacks and other threats related to an attacker forcing a cookie into the user's browser.
- Not allow for subdomains to overwrite the cookie, which protects against similar attacks from compromised/malicious subdomains.
Set-Cookie: __Host-foo=bar ...options...
By default web browsers cache everything they see to speed up page loads and save network bandwidth.
Caching is a synonym for storing visited websites and downloaded files on disk unencrypted until someone manually deletes them.
Your application users should be able to trust that once they hit "log out" then subsequent computer users will no longer be able to access their information (think shared library computers, friend's pc, etc).
For this reason, there is a header called
Cache-Control which you should return appropriately in all HTTP responses that contain non-public/non-static content.
Cache-Control: no-store, max-age=0
Serve a Clear-Site-Data header upon log out to protect your user's data from subsequent computer users
Another useful header for ensuring that user data is cleared upon logout is the new
You can send it as follows:
Ensure that logging out invalidates the access token/session identifier so that when it later leaks to an attacker from browsing history/cache/memory/etc. it will no longer be usable.
Additionally, if there is an SSO then don't forget to call the single logout endpoint properly, otherwise logging out would be in vain since merely clicking the "log in" button would automatically log the user back in as the SSO session is still active.
Finally clear any cookies, HTML5 storage, etc. that you might have stored the user's information in. The
Clear-Site-Data mentioned above is not yet supported by Safari for example so you will have to clear the data manually as well.
It's like LocalStorage but unique for each tab and clears after the browser/tab is closed. So there's a chance of user data leaking to the next computer user.
If you want to have your user be authenticated in multiple tabs of your application without having to log in again, you will have to use events to sync the sessionStorage between the tabs.
URL addresses are not designed to be secret. They are for example displayed on the screen, saved to browsing history, leaked with referrer-headers, and saved on server logs. So don't put secrets in there.
By default when you link to a website from your application, and a user clicks the link, web browsers will send a
Referrer header along with the request to tell the website which website linked to it. This header includes the entire URL which can be a privacy issue at the least.
You can disable this behavior by specifying a
Referrer-Policy header in your HTTP responses:
Use a unique domain name for your application to protect it from other applications under the same origin (and vice versa)
It is dangerous to host applications like this:
https://www.example.com/app2/. This is because browsers consider both of them to be of the same
origin (same host, port, and scheme), which means that they will have full access to each other and as such any vulnerabilities/malicious content affecting app1 would then also put app2 into danger.
For this reason, give each application an origin of their own. So the solution could be
Subdomains that share a parent can still set cookies for the entire domain. For example,
app1.example.com can set a cookie on
example.com which will then also be sent to
app2.example.com. This can make certain session fixation vulnerabilities possible.
And if you are now wondering if all applications under .herokuapp.com are vulnerable, the answer is no because of the public suffix list. Also, cookies can be protected from getting overwritten by subdomains by naming your cookies `__Host-`. This mechanism is described later in this article.
The web browser's security model is largely based on the Same Origin Policy which prevents
evil.example.com from reading your emails but still allows you to use jQuery from
code.jquery.com. CORS or Cross Origin Resource Sharing is a means by which you can allow another website to violate that policy.
So if you decide that you need it, make sure you know what you are doing.
If you have
api.example.com that needs to be accessed by GET requests from
www.example.com then you can specify the following header on
If you have multiple domains that you want to allow but not all (say you want to allow only Google and Facebook to access your API) then you will have to read the
Origin header from the request, compare it to a list of allowed domains and then return a header as appropriate. It is recommended to use a well-vetted library for this instead of messing with the headers manually because a lot could go wrong.
CORS by default does not allow credentialed requests, that is, requests that carry the user's (session) cookies. But this can be allowed by the webserver by specifying headers such as:
Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Credentials: true
This is dangerous as it would allow
https://www.example.com to fully access the website that specified the header just as the logged-in user would. So if you have to use it be very careful.
It's a good practice to minimize the attack surface and only allow the HTTP methods that you need.
If you don't need CORS then just don't use it, by default it's not enabled.
WebSockets are still pretty new, a bit scarcely documented and there are dangers involved when using them. So read the following carefully.
Just like you should use
https:// instead of
wss:// instead of
HSTS also affects WebSockets and will automatically upgrade unencrypted WebSocket connections to
wss://! Hail HSTS.
If you use cookie-based authentication and the WebSocket server is on the same domain as the application, you can keep using the existing session for the WebSocket connection as well. Just heed the next section about origin validation or you will be screwed.
If not then you could create a ticket in the application, that is, a single-use, time-limited authentication token bound to the user's IP address that can be used to authenticate the WebSocket connection.
A crucial thing to understand about WebSockets is that they are not bound by the Same Origin Policy. This means that any website out there can open a WebSocket connection to your application, and if you use cookie-based authentication, access the logged-in user's information.
For this reason, you must verify the origin of the connection in the WebSocket handshake. You can do this by validating the
Origin request header.
If you want double security, throw in a CSRF token as a URL parameter but create a single-use unique token for the job, do not use the CSRF token that you use to secure the rest of the application (because sending something in the URL can leak in many places).
If your threat model includes phishing attacks, that is, "what if an attacker creates a fake website that steals the username, password and the MFA code from our administrator/CEO/etc", then you should protect against such attacks with U2F tokens or client certificates, neither of which can be forged even if the attacker has the username, the password, and the MFA code.
Enforcing phishing protection is usually overkill for regular users, although there is nothing wrong with offering the possibility for the end-users to use e.g. their YubiKeys with the service if they so choose. What you can always do though is show the users a general heads up about phishing attacks.
Creating the Unphishable Security Key
Validate all input as strictly as you can. This will make many vulnerabilities difficult to find and exploit for attackers. Reject invalid input, do not sanitize it.
- Use restrictive data types. DateTime for dates, Integer for numbers, and so on. Use Enums for lists of possible values. Avoid using String when you can.
- When you do have to use String, put a length limit to it if you can.
- When you do have to use String, restrict the character set to the minimum.
- If you process JSON, use a JSON schema.
- If you process XML, use an XML schema.
Never show stack traces or similar debugging information to end-users. Have a global exception handler ready that catches otherwise unhandled exceptions and displays a generic error message to the browser. This will make it more difficult for an attacker to find and exploit vulnerabilities in your application.
There are just too many things that can go wrong when authenticating users. Defending against different kinds of password guessing and user enumeration attacks, managing password resets, storing the credentials, etc. is not easy. It's almost like with cryptography: mere mortals shouldn't do it by themselves.
Instead use an identity provider such as
auth0 for authenticating the users and implement the protocol (usually
OpenID connect) in your application using widely used and secure software components. If you don't want to use a third party IDP like auth0 then you can self-host something like
Configure your application so that everything is authenticated by default. Then create the necessary exceptions for static assets and perhaps some endpoints like a landing page or a "signed out" page.
If you want to include "what if someone fully compromises the IDP (Identity Provider)?" into your threat model, use some form of MFA (Multi-Factor Authentication) in your application. This way even if the IDP gets hacked and the attacker can authenticate as anyone there, the attacker will still not know the user's MFA secrets for the application itself.
Access control is not always easy but it can be done right. Just be centralized about it so that you won't end up with an IDOR (Insecure Direct Object Reference) vulnerability because you forgot to check the user's access in some individual controller function.
- Prevent access to all controller methods (or equivalent) by default.
- Allow access to individual controllers by role.
- Use method level security to also restrict access to e.g. service functions.
- Use a centralized permission evaluator to prevent unauthorized access to individual records.
- Use a centralized permission evaluator to filter objects returned to the client.
- Use an architecture with e.g. a frontend web app and a backend API then implement the same access controls in every app/API, not just the Internet-facing parts.
To clarify the permission evaluator approach a little bit, here's the crux of it:
- Your data records extend a class that has some property that you use for access control. For example
- Your authenticated user has an ID.
- You have a permission evaluator class, which knows that users can have access to objects if the object's
ownerIdequals the user's
- You then plug that permission evaluator into your application platform's access control system, such as Spring Security's PreAuthorize, PostAuthorize, PreFilter, PostFilter, etc.
- If you need more complex access control than
ownerIdor similar, then you can setup (for example) a full ACL system.
Multiple vulnerabilities fall under the category "injection" and they're all alike. These include SQL injection, HTML injection (a form of XSS), XML injection, XPath injection, LDAP injection, command injection, template injection, SMTP injection, response header injection... there are so many "different" vulnerabilities that are in reality the same issue with the same remedy:
- Issue: Using string concatenation/formatting to construct a parameterized message of protocol X.
- Solution: Use a proper, well (security) tested software library for the job and use it properly.
We won't go through each of the injection vulnerabilities in this article since the list would be infinite, so just remember this rule whatever protocol you're constructing. We'll cover some of the more prevalent/interesting ones though, such as SQL injection which is next on our list.
To avoid SQL Injection vulnerabilities, never construct SQL queries by string concatenation. Use an ORM (Object Relational Mapper) if you can, this will make development quicker and the application more secure.
If you want to have granular control over your queries, use a low-level ORM (often referred to as a query builder).
If you cannot for any reason use an ORM then go for prepared statements, but be careful as they are far more prone to human error than an ORM.
ORM frameworks are not a silver bullet in two senses.
First is that they still have functionality for supporting raw SQL queries/parts of queries. Just don't use those features and you're golden.
The second is that ORM frameworks have vulnerabilities from time to time, just like any other software package. So follow other good practices: validate all input, use a WAF and keep your packages up to date, and you're golden.
If you can avoid it, don't execute OS commands at all. It's always a bit dodgy.
If you have to do it, you can avoid command injection vulnerabilities and related issues by following these guidelines:
- Use a proper library/function to construct and parameterize the command. The parameters should be of the
listdatatype. Never create the command as a single string.
- Do not use a shell to invoke the command.
- Predetermine the parameters that you feed into the command. Using
curlas an example, by allowing the user to specify the
-oparameter you would allow the attacker to write to the local filesystem.
- Understand what the program does and validate the parameters appropriately. Again using
curlas the example, you might want to allow the user to retrieve websites such as
https://www.appsecmonkey.com/but what if the attacker retrieves
- Think through. Even if you validate that the parameter starts with
https://, would you like for the attacker to access
http://192.168.0.1/internal_sensitive_service/adminor do a port scan of the internal network?
Really think through. Even if you validate that the parameter is a valid DNS hostname that doesn't contain e.g.
yourcompany.local, is there anything preventing the attacker from creating a public DNS record that points
192.168.0.1? The answer is... no. It can be done.
XML is a dangerous markup language that contains features for accessing system resources, and some implementations of XSLT even support embedded code. For this reason, you must be extremely cautious when processing it.
- Avoid accepting XML/XSLT from untrusted sources if you can.
- If you parameterize XML, XSLT, or XPath expressions, use a proper software component for doing so. This is to avoid injection vulnerabilities. Don't use string concatenation/formatting/etc.
- Use a well known and thoroughly (security) tested software component for parsing XML/XSLT. This is crucial. Do not use a bad library or your code for handling XML. Furthermore, under any circumstances do not attempt to create a custom implementation for handling XML signatures (such as SAML), because there are so many things that can go wrong.
- Configure your parser properly. Disable
documentfor XSLT. Disable
xinclude. Disable document type definitions. Disable external entities. Enable DOS protection. The specific options will vary on the implementation, do some research on your chosen parser.
URL injections happen when you have something like this:
flavour = request.getParam("flavour");
url = "https:/api.local/pizzas/" + flavour + "/";
And someone enters a value like this:
This results in the API call returning a response for
https://api.local/admin/all-the-sensitive-things/ instead of the pizza endpoint like the developer intended.
And the solution, as always, is to use a proper URL construction library to parameterize the URL so that the values get properly encoded.
Just like URL addresses, file paths can also end up pointing to unwanted locations if an attacker manages to sneak a
../../../ sequence somewhere in the path. To avoid this, create a class that constructs the path safely and validates that the final path is in the intended directory. Avoid using untrusted data in the file path, or better yet, avoid using the filesystem altogether and prefer e.g. cloud storage instead.
There is an infinite list of things that can go wrong when allowing your users to write the server's filesystem. Use cloud storage instead, or if that doesn't work for you, use binary blobs in a database.
If you absolutely must access the disk, these guidelines could help you be safe:
- Be very careful not to allow any untrusted data to affect any part of the internal file path.
- Keep the files in an isolated directory far from e.g. the webroot.
- Validate that the file contents match the expected format before writing to disk.
- Set your filesystem permissions properly to prevent writing to unwanted locations.
- Don't extract compressed (e.g. ZIP) archives, as they can contain any files including symlinks and paths to anywhere on the system.
eval or equivalent functions. Find a way to achieve your goals without them. Otherwise, there will be a risk that untrusted data reaches the function call and someone will execute arbitrary code on your server.
Deserialization of untrusted data is a dangerous operation and can easily lead to remote code execution.
- Don't use serialization if you can avoid it.
- If you can serialize the objects on the server-side, then sign them digitally. And when it's time to deserialize them again, validate the signature before proceeding with deserialization.
- Use a well-known software component for the job and keep it rigorously up to date. Vulnerabilities are discovered in many deserialization libraries all the time. GSon is not a bad choice.
- Use a simple text format such as JSON instead of binary formats. Also, problematic formats like XML should be avoided because then you have XML vulnerabilities to worry about in addition to deserialization ones.
- Validate the serialized object before processing it. For example in the case of JSON, validate the JSON document against a strict JSON schema before proceeding with deserialization.
Put a web application firewall product in front of your application. This will make many vulnerabilities significantly harder to find and exploit. ModSecurity is a good open-source option.
There is an attack called "HTTP Desync" or "Request Smuggling", which could allow for an attacker to do all sorts of nasty things, such as steal HTTP requests of random users collecting to the web application, if the following conditions are true:
- There is a frontend web server, such as a load balancer/any reverse proxy, that accepts requests with both,
Transfer-Encodingheaders, and passes them on without normalizing the request.
- The next web server on the line, such as an application web server, uses, or can be tricked to use, a different mechanism than the frontend webserver to determine where the HTTP request begins and where it ends, e.g. the frontend would use
Content-Lengthwhereas the application server would use
- The front-end web server reuses the connection to the backend web server.
- The frontend web server uses HTTP/1 (instead of HTTP/2) in the backend server connection.
So how to protect yourself? Depends on the product but in general:
- Consult the documentation/vendor of the e.g. reverse proxy products that you are using and ensure that they are actively defending against the attack.
- Configure the front-end webserver to use HTTP/2 in backend connections.
- Configure the front-end webserver to prevent aggregation of HTTP requests from separate client-side TCP streams into the same server-side connection.
- Use a WAF (Web Application Firewall) and ensure it has a module for thwarting request smuggling attempts
Run your application in isolation so that in the event of a breach the attacker will not have unnecessary access to unwanted file-, system-, or network resources. So preferably use something like Kubernetes or a cloud serverless stack for deploying your application. If you are for any reason forced to use a bare server then manually run e.g. Docker to constrain the application.
Even if you run your application in a container, it's worthwhile to further constrain it with an SELinux or AppArmor policy. This will make exploiting container escape vulnerabilities very difficult, among other benefits.
This will usually limit damage when something goes wrong. Again an exhaustive list is impossible, but here are a couple of examples to get the idea:
- Even if you use Docker, and even if you use SELinux/AppArmor, do not run the application as root. This will make container escape/kernel vulnerabilities and other nasty tricks harder for an attacker to exploit. Create a specific user for the application with minimal permissions.
- If you have databases, ensure the application's database user has minimum access to tables, columns and dbms functionality.
- If you integrate with API's, ensure the application has minimum permissions to access the API.
An attacker often needs some kind of reverse communication channel to establish a command & control channel and/or to exfiltrate data. Also, several vulnerabilities require an egress network connection to be discovered and exploited.
For this reason, you should not allow arbitrary connections from your application to the outside world, and this includes DNS. If you can run
nslookup www.example.com successfully from your server then you haven't restricted egress properly.
How you would go about this depends on your infrastructure.
Egress TCP/UDP/ICMP can usually be disabled with one or more of the following:
- A gateway level firewall if you have one.
- Local firewall (e.g. iptables or Windows Firewall) if you have a old-fashioned server.
- iptables if you run Docker on your server.
- NetworkPolicy definitions if you use Kubernetes.
DNS is a bit more tricky since often times it is required to allow it for some hosts.
- If you can get away with a local hosts-file then perfect, this is a simple solution and you can disable DNS completely (with any of the techniques in the previous list).
- If not, then you have to configure a private zone in your upstream DNS and limit access on network level to that DNS server only. The zone should only resolve a predetermined list of hostnames.
Subdomain takeovers happen like this:
- You have a domain
- You buy another domain
www.my-cool-campaign.comfor a campaign and you create a
- Your campaign ends, and eventually
- You still have the
campaign.example.comto the expired domain.
- An attacker buys the expired domain, and now there is a DNS record under your domain (
campaign.example.com) which is pointing to an attacker controlled domain.
- The attacker hosts malicious content under
www.my-cool-campaign.comwhich will be accessible from
So be mindful about your DNS records. If you have to deal with lots of domain names like this, an automated solution for monitoring is highly recommended.
You shouldn't put too much trust in your Internet-facing web application. For example, it shouldn't have direct access to a database. Otherwise, when someone breaks into the Internet-facing application your entire database will be lost.
Instead separate your architecture into multiple components, for example:
- Your web application on
www.example.comwill authenticate your users on
- Your web application on
www.example.comis allowed to connect to the internal API
api.example.localwith the authenticated user's
auth0) which will then be passed as the
Authorizationheader when making calls to the internal API.
- Your API on
api.example.localwill enforce access controls based on the (end user's) access token and read/write the database appropriately.
Now if an attacker fully compromises your
www.example.com application, the attacker will not have full access to the entire database, but only individual user's data whose access tokens happen to be in the memory at the time.
Do not trust your internal network to be secure, there are many ways in which it could be compromised. Encrypt all system-to-system connections with TLS (that is, use HTTPS) and authenticate the connections preferably on both, network and application-level:
- Web App -> API: This is my client certificate. It's signed by the CA that we trust, and it says "CN=WebApp".
- Web App <- API: And this is my server certificate. It's signed by the CA that we trust, and it says "CN=API"
- Web App -> API: This is my access token that is signed by the IDP that we trust, I got it with OAuth2 client credentials grant flow.
- Web App -> API: ...and this is the access token of the logged-in user "John Doe" on whose behalf I'm making this request that was also signed by the IDP that we trust.
- Web App -> API: ...so could you give me John Doe's information, please?
- Web App <- API: Gladly. Since this is an encrypted and mutually authenticated connection network level, and because you seem to be "Web App" on the application level, and because you seem to be operating with the permissions of "John Doe".
Without a proper secrets management solution it is not easy to keep credentials short-lived, audit-logged, and not to expose them to human eyes. For this reason (and many others) it is recommended to use a tool such as HashiCorp vault to centrally manage integration secrets, encryption keys, and the like.
Collect logs centrally to a system, such as a SIEM (Security Information and Event Monitoring), where you can trigger alerts for specific events that indicate a vulnerability or an attack. Configure alert channels so that the relevant people will know immediately when a significant threat occurs.
Probably the most important log source is your application itself. You should raise exceptions when suspicious behavior happens, log the events and possibly even automatically lock out users/IP addresses that seem to be causing trouble.
Such events can be (these are just examples, the specific cases depend heavily on your application):
- Input validation errors (e.g. trying to give values for parameters that shouldn't have been possible through the UI).
- Access control errors (e.g. trying to access a record which shouldn't have been possible through the UI).
- Database syntax errors indicate that someone has discovered a SQL injection vulnerability and you need to move fast.
- XML errors indicate that someone has discovered an XML injection vulnerability or possibly is trying to find/exploit an XXE (XML External Entities) vulnerability.
- Bad request errors that indicate the end user sent something which was rejected by the application. Spring framework's RequestRejectedException is an example of this.
- CSRF token validation errors usually mean that someone is looking for vulnerabilities in your application.
Use a runtime security monitoring tool such as Falco to detect anomalous system calls. Falco is especially useful if you happen to use Kubernetes. Remotely collect and monitor these logs as well.
If you have an SELinux policy that prevents outgoing connections, and your application suddenly tries to make an HTTP request to e.g.
burpcollaborator.net, it would be very useful to know about it right away. Or perhaps your application tries to access
/etc/passwd. Both of these would indicate that someone has already found a serious vulnerability in your application.
Collect at least access logs and error logs from your web server software and send them to the central logging server as well. This will help in mapping the timeline in incident response.
If you use a WAF like recommended above, collect those logs as well. But don't necessary trigger alerts from them because generally WAF products get bombarded with all sorts of crap from the Internet that most of the time you won't have to worry about.
Once you have your monitoring and hardening in place, vulnerabilities will not be easy for attackers to find, vulnerabilities will be slow to successfully exploit, and you will know about the attempts quickly. Good place to be.
But knowing about attacks and slowing down attackers is not enough, you still have to do something about them. So have the people, tools, and processes ready for:
- Quickly analyzing the logs and understanding what is happening and what needs to be done
- Quickly restricting individual URL addresses or parameters in e.g. an application firewall product
- Quickly shutting down the application if needed
Go through a process of thinking "what could go wrong" and then do something about it. Preferably do this from the get-go when you start designing a system, but it's never too late to begin, and at any rate you should re-visit this process when you introduce changes into the system.
Jim: What if an attacker breaches the Internet facing web server?
Bob: Well then we'd be royally screwed.
Jim: Okay so we have a trust relationship there, we trust that the Internet facing web server will not be pwned. Can we really trust that?
Bob: Well no, there are a gazillion things that could result in that thing getting hacked, for example vulnerabilities in our own code, or vulnerabilities in a dependency that we use, or perhaps vulnerabilities in our web server software.
Jim: Right. So let's break that trust relationship. But how?
Bob: Let's break the monolith and create an internal API that does the actual database access. Then the frontend web server will not have access to everything at once.
Jim: Great idea. So what else could go wrong?
Bob: Well what if an attacker breaches our internal network?
Jim: All would be lost, the server-to-server connections are all unencrypted.
This is threat modeling and it doesn't have to be complex or scary. Use it to discover dangerous trust relationships and then break those relationships.
Implement a technical control that prevents code from entering the repository without at least one or two other developers approving it. This is the basis of your secure development lifecycle because now two things happen:
- If an attacker compromises the workstation of a developer, or the developer goes rogue, it will not be possible to directly push malicious code into the repository.
- If a developer makes a mistake and tries to introduce vulnerable code into the repository, there is a good chance that the other developers reviewing the code will catch the error before it gets merged.
Individual developers should be able to trigger e.g. a Jenkins build, but Jenkins should be configured to allow that and nothing else. Individual developers should not be able to introduce arbitrary code into the build phase. You can however keep the Jenkinsfile in source control as long as the peer review process is technically forced like recommended above.
Sign the artifacts. For example, if you are building a container image, sign the image as part of the build. Store the signing keys safely. The build phase needs to access the keys but they shouldn't be stored in version control with the Jenkinsfile. Preferably keep the keys in e.g. HashiCorp vault and pull them at build time.
Run a tool such as SpotBugs + FindSecBugs (or a similar tool applicable to your technology of choice) in your CI pipeline. This will help you spot some known vulnerabilities in your code before deploying it.
You can additionally run these tools on the developer's workstation (as an IDE plugin for example) to catch issues even before checking them to version control.
Every software package that you depend on is a risk. You are pulling code from someone elses' repository and executing it on your application server. So be mindful about what and how you depend on.
- Keep the dependencies at a minimum.
- Only use dependencies that you trust. They should all be widely used and reputable.
- Use a build framework that supports dependency verification, and make sure the verification is enabled.
As additional hardening restrict egress connections from your application server (described earlier in this article) to prevent any backdoors from "calling home".
Run a tool such as OWASP DependencyCheck as part of your CI pipeline to catch some dependencies you might be using that have known security issues in them.
You run these tools on the developer's workstation as well (but also run them in the CI pipeline that's the most important thing).
If you use containers, use a tool such as Trivy to scan the created container image for known vulnerabilities.
Individual developers could well have the right to deploy to production, but only the specific images built and signed in the previous stages should be deployable. Access to production secrets or direct access to the servers should not be possible. Validate signature of the deployment image, for example, if you are using Kubernetes then validate the container signature via e.g. Notary and Open Policy Agent.
There is a limit to how much a single person can obsess about. You cannot expect every developer to be a masterful penetration tester or security engineer. Just as you can't expect all security professionals to be outstanding developers.
So it's generally a great idea to introduce people to your team with a security focus, for sparring with developers, architects etc. and helping to secure your applications and spread security awareness within the team.
There is much more to securing your application than avoiding vulnerabilities. To summarize some of the main ideas:
- Use up to date, modern, well-known software components for performing risky operations such as authentication, access control, cryptography, accessing a database, or parsing XML. And make sure you have configured those components properly, for example by disabling external entities in your XML parser.
- Use the security controls offered by your platform, for example, CSRF protection.
- Use the security controls offered by web browsers, such as HSTS, SameSite cookies, and Content Security Policy.
- Centralize your security controls, especially authentication and access control, to avoid vulnerabilities where you "forget to add security" to some controller function, etc.
- Use a web application firewall to make finding and exploiting many classes of vulnerabilities in your application difficult.
- Contain your application by restricting its access to file-, network-, and system resources.
- Threat model to discover any dangerous trust relationships in your architecture, then break them. This could include for example source control policies to break the trust relationship to the integrity of each developer's workstation, and a clever architecture to break the full trust in frontend webserver not getting compromised.
- Monitor vigorously and have a plan when something goes south.
- Use code/image/dependency vulnerability scanners in both development environments and the CI pipeline.
- Educate developers, architects, etc. about security and have a security champion on the team.
☝️ Subscribe to AppSec Monkey's email list, get our best content delivered straight to your inbox, and get our 2021 Web Application Security Checklist Spreadsheet for FREE as a welcome gift!
If you like this article, check out the other application security guides we have on AppSec Monkey as well.
Thanks for reading.