Teo Selenius
Posted on February 16, 2021
The original post can be read here.
Get the spreadsheet!
Subscribe to AppSec Monkey now and get the 2021 Web Application Security Checklist Spreadsheet as a welcome gift for FREE!
Overview
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
- Use JavaScript 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
- Use SessionStorage for JavaScript secrets to protect your user's 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
- Conclusion
Defending Threats On The Browser Side
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).
Use HTTPS and only HTTPS to protect your users from network attacks
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.
Further reading
Use HSTS and preloading to protect your users from SSL stripping attacks
HSTS or 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.
Example
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Warning
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-age
and 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.
Further reading
Serve cookies with the 'Secure' attribute to protect your user from network attacks
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
Further reading
Generate HTML safely to avoid XSS vulnerabilities
To avoid XSS (Cross-Site Scripting) vulnerabilities, use one of the following:
- Completely static websites (e.g. JavaScript SPA + backend API). The most effective way to avoid problems with generating HTML is not to generate HTML at all.
- 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
Twig
for PHP,Thymeleaf
for Java,Jinja2
for 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.
Further reading
Use JavaScript safely to avoid XSS vulnerabilities
To avoid XSS (Cross-Site Scripting) vulnerabilities on the JavaScript side, don't pass any untrusted data into functions or properties that could end up executing code. You have to use common sense here but some of the usual suspects are:
-
eval
,setTimeout
,setInterval
, etc. -
innerHTML
, React'sdangerouslySetInnerHTML
, etc. -
onClick
,onMouseEnter
,onError
, etc. -
href
,src
, etc. -
location
,location.href
, etc.
Further reading
Sanitize and sandbox untrusted content to avoid XSS vulnerabilities
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.
Further reading
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.
Additionally, the 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 Report-Only
mode:
Content-Security-Policy-Report-Only: default-src 'self'; form-action 'self'
To generate a CSP policy for your website, use the CSP tool
Further reading
Serve cookies with the HttpOnly attribute to protect them from XSS attacks
Configure your cookies with the HttpOnly
attribute. This will prevent them from being accessed by JavaScript code, which in turn makes them harder for an attacker to steal in the event of a successful XSS (Cross-Site Scripting) attack. Of course for the cookies that need to be accessed by JavaScript, you cannot do this.
Set-Cookie: foo=bar; ...other options... HttpOnly
Further reading
Serve downloads with a proper Content-Disposition header to avoid XSS vulnerabilities
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 attachment
to inline
for that particular file extension/extensions.
Further reading
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"
Further reading
Use your platform's anti-CSRF mechanism to avoid CSRF vulnerabilities
To protect against Cross-Site Request Forgery (CSRF) vulnerabilities, ensure that your platform's anti-CSRF mechanism is enabled and working as intended.
Further reading
Validate the OAuth/OIDC state parameter to avoid CSRF vulnerabilities
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.
Further reading
Use HTTP verbs properly to avoid CSRF vulnerabilities
Never use anything except for POST
, PUT
, PATCH
or DELETE
for making any changes. GET
requests for example are usually not covered by anti-CSRF mechanisms.
Further reading
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
and Strict
.
The 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.
However the 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 SameSite
in 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
...or:
Set-Cookie: foo=bar; ...other options... SameSite=Strict
Further reading
Create a fresh session ID on login to protect against session fixation attacks
- An attacker sneaks a cookie, say,
JSESSIONID=ABC123
into 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=ABC123
cookie 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.
Further reading
Name your cookies right to protect against session fixation attacks
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...
Further reading
Serve proper Cache-Control headers to protect your user's data from subsequent computer users
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
Further reading
Cache-Control
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 Clear-Site-Data
header. You can send it in an HTTP response when the user logs out, and the browser will clear the cache, cookies, storage, and execution contexts (JavaScript variables, etc. probably by refreshing all relevant tabs, this has not yet been implemented at the time of this writing) for the domain. Most browsers support it, Safari notably still doesn't.
You can send it as follows:
Clear-Site-Data: "*"
Further reading
Clear-Site-Data
Log your users out properly to protect their data from subsequent computer users
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.
Use SessionStorage for JavaScript secrets to protect your user's data from subsequent computer users
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.
Note
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.
Further reading
Session Storage
Don't transmit sensitive data in the URL because URLs are not designed to be secret
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.
Use a referrer policy to prevent URL addresses from leaking to other websites
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:
Referrer-Policy: no-referrer
Further reading
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/app1/
and 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 https://app1.example.com/
and https://app2.example.com/
.
Note
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.
Further reading
Don't use CORS unless you have to, and if you have to, be careful with it
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.
Validate the origin
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 api.example.com
:
Access-Control-Allow-Origin: https://www.example.com
If you have a public API (let's say a calculator that you want the entire Internet to use from client-side JavaScript) then you can specify a wildcard origin:
Access-Control-Allow-Origin: *
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.
Be mindful about the "allow credentials" option
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.
Validate the method
It's a good practice to minimize the attack surface and only allow the HTTP methods that you need.
Access-Control-Allow-Methods: GET
Note
If you don't need CORS then just don't use it, by default it's not enabled.
Further reading
Use WebSockets properly to avoid CSRF and other vulnerabilities
WebSockets are still pretty new, a bit scarcely documented and there are dangers involved when using them. So read the following carefully.
1. Encrypt the connection
Just like you should use https://
instead of http://
, use wss://
instead of ws:///
.
HSTS also affects WebSockets and will automatically upgrade unencrypted WebSocket connections to
wss://
! Hail HSTS.
2. Authenticate the connection
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.
3. Verify the origin of the 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).
Further reading
Use U2F tokens or client certificates to protect your critical users from phishing attacks
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.
Note
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.
Further reading
Creating the Unphishable Security Key
Defending Threats On Server Side - Application
Validate input properly to protect your application from... so many vulnerabilities
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.
Catch exceptions gracefully to avoid leaking technical details
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.
Don't do authentication yourself
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 KeyCloak
.
Further reading
Authenticate everything to reduce the attack surface
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.
Use MFA in your application to break the trust relationship to the identity provider
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.
Use strict access controls to prevent unauthorized access to data or functionality
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
int ownerId
. - 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
ownerId
equals the user'sid
. - 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
ownerId
or similar, then you can setup (for example) a full ACL system.
Further reading
Use proper tools and techniques to avoid injection vulnerabilities
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.
Construct database queries safely to avoid SQL injection vulnerabilities
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.
Warning
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.
Further reading
If you must run OS commands, do it right to avoid command injection and related vulnerabilities
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
list
datatype. 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
curl
as an example, by allowing the user to specify the-o
parameter you would allow the attacker to write to the local filesystem. - Understand what the program does and validate the parameters appropriately. Again using
curl
as the example, you might want to allow the user to retrieve websites such ashttps://www.appsecmonkey.com/
but what if the attacker retrievesfile:///etc/passwd
instead? - Think through. Even if you validate that the parameter starts with
http://
orhttps://
, would you like for the attacker to accesshttp://192.168.0.1/internal_sensitive_service/admin
or 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 pointswww.example.com
to192.168.0.1
? The answer is... no. It can be done.
Further reading
Avoid XML vulnerabilities by configuring your parsers properly
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
document
for XSLT. Disablexinclude
. 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.
Further reading
Avoid URL injection vulnerabilities by using proper class for URL construction
URL injections happen when you have something like this:
python
flavour = request.getParam("flavour");
url = "https:/api.local/pizzas/" + flavour + "/";
return get(url).json();
And someone enters a value like this:
../admin/all-the-sensitive-things/
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.
Avoid path traversal vulnerabilities by using a proper class to construct the paths
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.
Further reading
Don't use the filesystem for untrusted content (e.g. uploads) if you can avoid it
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.
Don't execute dynamic code to avoid remote code execution vulnerabilities
Don't use 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.
Use serialization carefully to avoid deserialization vulnerabilities
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.
Further reading
Defending Threats On Server Side - Infrastructure
Use a WAF
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.
Further reading
Configure your web server carefully to avoid HTTP desync attacks
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,
Content-Length
andTransfer-Encoding
headers, 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-Length
whereas the application server would useTransfer-Encoding
. - 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
Further reading
Use containers
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.
Further reading
Use SELinux/AppArmor
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.
Further reading
Use service accounts with minimum privileges
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.
Restrict egress network connections
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.
Keep track of your DNS records to prevent subdomain takeovers
Subdomain takeovers happen like this:
- You have a domain
example.com
. - You buy another domain
www.my-cool-campaign.com
for a campaign and you create aCNAME
fromcampaign.example.com
towww.my-campaign.com
. - Your campaign ends, and eventually
www.my-cool-campaign.com
expires. - You still have the
CNAME
pointingcampaign.example.com
to 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.com
which will be accessible fromhttps://campaign.example.com
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.
Further reading
Defending Threats On Server Side - Architecture
Create an internal API for accessing data sources to get rid of dangerous trust boundaries
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.com
will authenticate your users onauth0
. - Your web application on
www.example.com
is allowed to connect to the internal APIapi.example.local
with the authenticated user'saccess-token
(obtained fromauth0
) which will then be passed as theAuthorization
header when making calls to the internal API. - Your API on
api.example.local
will 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.
Encrypt and authenticate all connections
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".
Manage secrets centrally
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.
Further reading
Defending Threats On Server Side - Monitoring
Collect, analyze, alert
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.
Collect application security events
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.
Collect runtime security logs
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.
Further reading
Collect SELinux/AppArmor logs
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 webserver events
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.
Collect WAF logs
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.
Defending Threats On Server Side - Incident Response
Have a plan
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
Secure Development Considerations
Threat model
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.
For example:
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.
Bob: ...
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.
Force peer review in source control
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.
Further reading
Automate the CI pipeline and restrict mere mortal access to it
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 build artifacts
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.
Further reading
Run a static application security scanner as part of the CI pipeline
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.
Further reading
Verify dependencies on build and keep them at a minimum
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".
Further reading
Run a dependency security scanner as part of the CI pipeline
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).
Further reading
Run a container image security scanner as part of the CI pipeline
If you use containers, use a tool such as Trivy to scan the created container image for known vulnerabilities.
Further reading
Automate deployments and validate signatures
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.
Further reading
Have a security champion
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.
Further reading
Conclusion
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.
Get the web security checklist spreadsheet!
☝️ 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!
Don't stop here
If you like this article, check out the other application security guides we have on AppSec Monkey as well.
Thanks for reading.
Posted on February 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 28, 2024
January 11, 2022