There is no software without bugs.
According to the SiteLock Website Security Insider report, there are 113 million websites that have security vulnerabilities, and the average website suffers 50 attack attempts per day. This amount is simply overwhelming.
There are many possible exploits that websites might be vulnerable to – malware attacks, script injections, file inclusions and so on.
And if that wasn’t bad enough, researchers from Northeastern University analyzed over 130K websites in 2018 and revealed that “the median website in their dataset is using a library version 1,177 days older than the newest release, which explains why so many vulnerable libraries tend to linger on the Web.” That’s over 3 years. Amazing!
What is even more interesting, WordPress, the popular content management system, explicitly indicates its version in the source code using the line:
|<meta name=”generator” content=”WordPress 3.0.0″ />|
So, even inexperienced attackers can search for websites running on outdated versions of WordPress (by simply pasting the above line into the search engine). If you use WPScan, it will reveal the vulnerabilities of specific versions and the ways of exploiting them.
Let’s take a look at some of the most common vulnerabilities and talk about how to prevent specific threats.
Cross-Site Scripting (XSS) is one of the most common vulnerabilities of web applications. It’s a type of attack which comprises of code embedding into a legitimate website by using the user input fields.
The code may be passed to the website in two different ways:
- By an unknowing user – a link with a malicious XSS code can be found in your e-mail inbox. Such a message may include a fake link used to confirm fake registration, and the script is brought on in one of the URL parameters. If the application allows the user to pass special characters in the website address, the script will be injected and executed as a legit part of the website. Clearly, phishing is used to carry out the injection.
Here is an example URL address which passes the script to execution with the parameter:
- By the attackers themselves – attackers often target input forms to check for a vulnerability and process this code. If a site immediately returns data that was passed – it indicates that the vulnerability exists.
Here is an example of immediately returned data passed to the application:
It will be processed as:
Cross-site scripting may harm your website in numerous ways, such as:
- stealing sensitive data (eg. credentials, session cookies),
- enabling keylogging, which records every pressed key and sends the data to an attacker,
- changing a website’s content.
The script may trigger a range of malicious behaviors – if an attacker writes code which sends your authorization cookies data to their server, they might be granted access to the session you’re currently logged in. While the risk of being seriously harmed is high (because it may be comprised of our data), there are numerous ways of defending yourself.
Preventing XSS attacks
First of all, the most universal way (which applies to any code injection) is to sanitize the inputs. Sanitization is an operation of replacing special HTML characters (eg. angle brackets, known as inequality signs, curly braces, etc.) into “safer” ones, called HTML entities. This ensures proper request processing.
Here is an example matrix of special characters and its decoded values:
|Input character||Encoded value|
|<||< or <|
|>||> or >|
|“ (quotation mark)||" or "|
|‘ (apostrophe)||' or '|
|& (ampersand)||& or &|
This header provides protection against XSS attacks and is supported by Chrome, Opera, Safari and Internet Explorer 8+. You can use one of four directives:
- X-XSS-Protection: 0 – disables protection entirely,
- X-XSS-Protection: 1 – protection is enabled. An attack, if detected, is prevented by sanitizing the whole page,
- X-XSS-Protection: 1; mode=block – protection is enabled, but the website won’t load if an attack is detected,
- X-XSS-Protection: 1; report=<reporting-URI> – protection is enabled. The page will be sanitized if an attack is detected and the attack details will be submitted to the provided URL.
A more advanced way to combat cross-scripting threats is to implement Content-Security-Policy (CSP) headers. This helps to block an inline script injection and control the sources of the external content. This means that the browser will only use resources that are allowed by specific CSP directives.
Content-Security-Policy headers have some useful directives:
- default-src ‘self’ allows the usage of sources that come only from the original domain and forces an HTTPS protocol. This method also blocks the usage of inline scripts. The header syntax for this definition is as follows:
|Content-Security-Policy: default-src https: ‘self’|
- script-src allows for the execution of scripts from indicated sources only:
|Content-Security-Policy: script-src https://trusted-source.com/;|
Usually, a line of code is added to the HTTP header, but CSP may be implemented as well in the head section of a website, which is usually easier to implement. The only thing to remember is to place your code directly below the <head> tag:
Apart from accepting the resources of the same origin, this policy prevents inline scripts from executing.
If we try to include an inline script within our website’s code:
The browser’s console will raise an error and indicate the corresponding line of code:
The above methods will allow you to be safer than you were at the beginning of this article.
But keep in mind, this isn’t the only type of code injection that exists.
An injection that could harm your databases is SQL Injection. It’s a code injection method that targets systems taking data from users and generating requests based on it. The problem occurs when:
- The user input is not properly sanitized, or
- The database accepts data having the wrong context.
SQL injection is the most popular attack that can be performed against database-driven applications. According to Veracode’s State of Software Security Report, 33% of websites have at least one SQL injection vulnerability.
An attacker might use SQL injection to:
- modify data (eg. user account balance),
- steal data (eg. user credentials, credit card information) and, as a result, spoof identity,
- destroy data.
The idea behind the injection is simple. Dynamically generated queries are the queries which take the user’s data as arguments. When the input is not sanitized, an attacker may use special characters (like single quotes, semicolons, slashes or asterisks) to change the original query in order to manipulate the data stored in the database.
This method may be used by criminals to gain access to authorization data, so our form will send a request and try to find a matching pair of username and password:
This form has an unsafe connection to the database using the query:
The query seen above tells us that if the username matches the password, the database will display data associated with that username. The most basic SQL injection, besides the data intended to pass, is comprised of an additional expression which will always evaluate to true. Because “1 and 1” will always be true, we may use it in order to exploit this vulnerability.
So, if we try with true, we should consider which expression may evaluate to true. And it’s simple – 1=1 will always be true. To “attach” this expression, we could use logical operators such as OR. If any condition we compare using this operator is true (and 1=1 is always true), the expression will be true.
The access to the user’s account will be granted, because the password will always evaluate to true. The query that will be passed to the database will look slightly different than the original:
If access to someone’s account is not enough for the attacker, they might even modify the query to a drop database.
Preventing SQL injection attacks
If you have to take the data from a user, consider using parameterized statements (also known as a prepared statement). This is an expression that processes data of a certain type. The procedure will strip out the unwanted characters from the input, and pass them straight into the query, which will be evaluated correctly. For example, if we want to insert the user’s data comprised of any numeric values, like the year of birth, we can specify the accepted data format as an integer. Now we are sure that this statement will be executed properly in an efficient way and no other data type than the integer will be accepted.
Another form of prevention that has to be considered is to follow the principle of least privilege. The main purpose of this rule is to allow specific parts of an application to access only the resources required in the specific process.
If a website wants to display content stored in the database, it should use a read-only SELECT statement. It really shouldn’t have the possibility to alter or delete the data.
Because it’s a really bad idea to store raw password data in the database, they need to be encrypted. To provide this, use password hashing (and salting). Hash function is a one-way function which decodes your password as a string of random characters. But using the hash function itself doesn’t provide enough safety. That’s why a salt – a random value assigned to a password – is added before hashing. This produces a unique hash value even if a common password is provided. The password is then impossible to crack.
Here is an example of password security algorithms in Django:
And of course, sanitize all the inputs and filter the data according to data context.
Now that our databases are safe, we should now take a look at another often overlooked vulnerability: open redirects.
Open redirects are one of the easiest website vulnerabilities to exploit. The idea is to abuse the trust of a legitimate website and use it to make a redirect to a malicious site.
This vulnerability might be exploited to conduct phishing attacks to steal user credentials. A possible scenario of an attack looks like this:
- Attackers are targeting a website vulnerable to open redirects,
- They create a fake website – a one-to-one copy of the legit website’s login page,
- They place a link to the malicious site as a redirect in the legit website’s URL (example.com/?redirect=http://malicious-website.com),
- The user will see the login page and type the credentials into a fake form,
- The credentials are sent to the attackers.
As we see, any unsafe open redirect may be used to carry on a phishing attack. If a social media app is vulnerable, it would be easier due to the user’s trust in the app. Users may see only the URL that is trimmed, so they may not even notice that they’re being redirected. Criminals also may use free SSL certificates to make the fraud attempt appear a little more trustworthy. When the malicious link is opened, the user sees the exact copy of the legit website. The following screenshot shows that the open redirects are not a relic of the past:
It seems even the most popular websites, like Google, Amazon and Facebook may be affected by this vulnerability. Some websites try to protect against this, and ask users if the action is legitimate. Facebook deals with it by showing an information box with options for following the link or discarding the redirect.
Preventing open redirect attacks
There are some methods of mitigating the risks carried out by open redirects. The safest one is not to use open redirects and forwards. It’s as simple as that.
But if you cannot avoid redirects including user input, ensure that users can’t decide where to redirect from your website. This might be achieved by the aforementioned input sanitization.
The best way to do this is to create a list of trusted sources. Only these values, when included in the list, will be passed to the URL correctly and redirected. Another solution is to build addresses with a redirection URL decoded into a string of numbers. It means that the user won’t see the exact redirection target in the URL. It might look like this:
But to ensure the user’s safety, prompt the user with a notice and let them decide what to do.
Cross-Site Request Forgery
Cross-Site Request Forgery is another vulnerability which targets user’s trust to the application. It won’t use injected scripts, it won’t harm your database, and it won’t try to redirect you to a hacker website. But it can harm your website badly.
Cross-Site Request Forgery, also known as CSRF, is a type of attack that involves an unaware user who sends a form, crafted by an attacker, to a vulnerable application. The attacker’s aim is to use the victim’s credentials and permissions to perform an action on behalf of the victim.
The forgery can:
- steal or change a user’s authentication data,
- even lead to a transfer money to the attacker’s account.
The main purpose is to force a user who is logged into a vulnerable application or website to open a link that triggers an specific action for the opened application.
For example, if a user is logged into a forum, they may open a malicious link or an intentionally broken image. Those broken images are not displayed correctly because instead of its proper address, the attacker has hidden malicious code in the image’s markup. Both the malicious link and the “broken” image could be posted by an attacker into a random thread. When opened, the attacker may change the user’s displayed name, delete a message and thread, or post something. If a forum’s admin clicks this link, the whole website could be compromised – this request may create another account with admin privileges. The consequences of such action could be unpleasant as CSRF attacks may also target accounts with users’ payment data.
The attack exploits application logic flaws. It is not intended to break application security – if an attacker successfully uses CSRF to force a money transfer from one account to another, the request will be identified as a legal action taken by the user. While it’s extremely dangerous, it’s quite easy to prevent.
Preventing CSRF attacks
The best way to mitigate and prevent cross-site forgeries is to use synchronized tokens. When a form submission request is sent (a POST request), the back-end logic randomly generates tokens.
The first is sent directly to the form, where it is stored in a hidden field, and the second is stored on the server in order to perform the validation. When the form is sent, the token validation is checked. If both tokens match, the form is processed. Otherwise, an error will be returned or nothing will happen at all.
This safeguard protects users from CSRF attacks, because the attacker who sends the request has to guess the token value. After the user logs out of the application, the token will be invalidated.
- Always use HTTPS. Ensure that any data the user sends to the application is secure.
- Sanitize your inputs and encode your URL addresses. Prevent injection attacks – the attacker won’t be able to harm you using forms and redirections.
- Use Content Security Policy headers as an afterburner. Decide whether your application allows inline or external sources, and specify those sources.
- Use data in proper context. Don’t process any kind of provided data as one type. Don’t allow any special characters to be passed to database.
- Hash your passwords. Make sure there won’t be any leak of the users’ personal data.
- If using open redirects, prompt users before performing a redirection. If you allow open redirects, make sure they’re not arbitrary.
- Use tokens. Generate and request authenticity in real time.
Security layers are the kind of features that should be discussed before the development process starts. Any vulnerability in your application may jeopardize any data that the users entrusted to you, which in turn might decrease the users’ trust with your app and eventually your brand.
And you don’t want that, do you?