Security
Table of contents
Loading...Introduction
Web application security is one of the most important aspects of web development and is every developer's responsibility.
I strongly recommend learning the basics of web application security and Survive The Deep End: PHP Security by Padraic Brady which explains the most common vulnerabilities very clearly in detail.
The focus in this chapter is the implementation of input validation, the protection against SQL injection through parameterized queries, cross-site scripting through output escaping, and brute force attacks with request throttling.
Input validation
Validation is the outermost layer of defense.
A significant majority of web application vulnerabilities arise from a validation failure, so getting this part right is essential. It's one of the most fundamental defenses that a web application relies upon.
Creating an own validation system that is robust and secure is time-consuming and error-prone.
Therefore, the task of validating user input is done with the help of the cakephp/validation
library.
The Validation chapter explains how the library is used.
Parameterized queries
Instead of directly including user input in the SQL statement, a placeholder
(like a question mark or a named parameter) must be used and the input is supplied
as a separate argument.
This ensures that the input is treated strictly as data and not as part of
the SQL command, thereby preventing SQL injection.
Even though the native PDO library supports parameterized queries, a QueryBuilder makes it easier to build secure queries.
The Repository
chapter explains how the database is accessed with the library cakephp/database
which
automatically uses parameterized queries
and supports the use of
named parameters
when raw SQL is needed.
Output escaping
Cross-site scripting (XSS) occurs when an attacker is able to inject a script (often JavaScript) into an application in such a way that it is executed in the browser of other users.
Its potential for damage is often underestimated. Injected JavaScript can be used among other things to steal session cookies, redirect users to malicious websites, or take over the UI via the DOM and disguise components to trick users into entering sensitive info or clicking on malicious links.
As protection, all output has to be escaped so that it's treated as plain text by the browser and not interpreted as HTML or JavaScript.
The frontend must not trust any values that are coming from the backend.
Template renderer escaping
Every value rendered in the
template renderer templates
has to be escaped with the
autoloaded
global function html()
.
Function declaration
File: config/functions.php
function html(?string $text = null): string
{
return htmlspecialchars($text ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
Usage
<?= html($client->firstName) ?>
JavaScript escaping
When rendering values in JavaScript, the same escaping has to be applied.
The JavaScript function html()
serves this purpose.
Function declaration
File: public/assets/general/general-js/functions.js
export function html(unsafeString) {
return unsafeString?.toString()
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
Usage
let html = `<p>${html(client.firstName)}</p>`;
Request throttling
Certain requests should be restricted to a specific number within a given time period to prevent potential harm to the application or server.
This is particularly crucial for tasks such as authentication or when dealing with an external API that imposes rate limits and may result in additional costs.
Authentication
A common method used to gain access to a user account is a brute-force attack.
This occurs when an attacker attempts to guess the password by systematically trying
many combinations, often with a list of commonly used passwords.
This attack involves sending a large number of requests to the server within a
brief timeframe.
Throttling can mitigate this risk by restricting the number of
requests to a predefined limit per time period.
The code for the throttling is in the
src/Domain/Security
namespace of the Slim Example Project.
Individual login requests
Login requests that involve one user or are coming from the same IP-address are throttled
with the setting $settings['security']['login_throttle_rule']
.
The key is the number of failed login requests, and the value is the delay in seconds.
As value, a number can be used to define a fixed delay, or the string 'captcha'
to require
the user to fill out a captcha form.
The throttling only applies to login requests in the past $settings['security']['timespan']
seconds.
To prevent bots from increasing the total number of login requests to manipulate the global threshold, the same limit is enforced for failed and successful login requests.
Global login rules
Instead of attempting the 1000 most common passwords for a single user, an attacker may opt to try
the single most common password across 1000 different users.
This type of attack is called password spraying.
As a protection for this, a global login failure threshold can be defined with the config value
$settings['security']['login_failure_percentage']
.
If the threshold value is set to 20, for example, a CAPTCHA is required for all users in the if the ratio of failed login requests exceeds 20% of the total number of login requests recorded in the past month.
If there are 200 logged login requests, the threshold is reached at 40 failed login requests.
This rule is only active when there is a significant number of total login requests (when the calculated failure threshold is more than 20).
To prevent an attacker from increasing the total login requests with a lot of successful requests on accounts they own and thus manipulating the global threshold, the individual successful login requests are throttled with the same limit as the failed requests.
Configuration
The default configuration values for the throttling are defined in config/defaults.php
.
// Bool if login requests should be throttled
$settings['security']['throttle_login'] = true;
// Seconds in the past that should be considered for throttling
$settings['security']['timespan'] = 3600;
// key = failed request amount for throttling to apply; value = delay in seconds or 'captcha'; Lowest to highest
$settings['security']['login_throttle_rule'] = [4 => 10, 9 => 120, 12 => 'captcha'];
// Percentage of login requests that may be failures globally (threshold). Timespan is one month.
$settings['security']['login_failure_percentage'] = 20;
Usage
On a login action, before verifying if the password is correct, the function to perform the login
security check is called.
If it fails, a SecurityException
is thrown storing the remaining delay.
Failed and successful authentication requests are logged in the database to make the future security checks.
The main login service class (i.e. LoginVerifier
) is responsible for
calling the performLoginSecurityCheck
function from the
src/Domain/Security/Service/SecurityLoginChecker.php
class and logging
the request with the logLoginRequest
method of the src/Domain/Authentication/Service/AuthenticationLogger.php
.
File: src/Domain/Authentication/Service/LoginVerifier.php
public function verifyLoginAndGetUserId(array $userLoginValues, array $queryParams = []): int
{
// Validate submitted values
$this->userValidator->validateUserLogin($userLoginValues);
$captcha = $userLoginValues['g-recaptcha-response'] ?? null;
// Perform login security check before verifying credentials (throws SecurityException if failed)
$this->loginSecurityChecker->performLoginSecurityCheck($userLoginValues['email'], $captcha);
// Verify credentials
// ...
// If successful, log request with true as second param
$this->authenticationLogger->logLoginRequest($userLoginValues['email'], true);
// return the user id
// If unsuccessful, log request with false as second param
$this->loginSecurityChecker->logLoginRequest($userLoginValues['email'], false);
// If credentials are invalid, the login security check is performed again to show the correct delay
// after this new failure.
$this->loginSecurityChecker->performLoginSecurityCheck($userLoginValues['email'], $captcha);
}
Email limit
To prevent spamming and other malicious activities, a limit should be placed on the number of emails that can be sent within a given time period.
Individual email requests
Throttling is applied to emails that are sent to the same recipient or sent from the same user.
With the values $settings['security']['user_email_throttle_rule']
, the threshold and the
delay for a next request can be defined for individual email requests.
This throttling only applies to emails being sent in the past $settings['security']['timespan']
seconds.
The key 'user_email_throttle_rule'
can be omitted if no individual throttling is desired.
Global email rules
A global daily and monthly limit can be defined with the config values
$settings['security']['global_daily_email_threshold']
and
$settings['security']['global_monthly_email_threshold']
.
When the threshold is reached, a captcha is required for anyone wanting to send emails.
These rules can also be omitted if no global throttling is desired.
Configuration
The default configuration values for the email throttling are defined in config/defaults.php
.
// Bool if email requests should be throttled
$settings['security']['throttle_email'] = true;
// Seconds in the past that should be considered for throttling (same value as for authentication)
$settings['security']['timespan'] = 3600;
// Optional configurations
// key = sent email amount for throttling to apply; value = delay in seconds or 'captcha'; Lowest to highest
$settings['security']['user_email_throttle_rule'] = [5 => 2, 10 => 4, 20 => 'captcha'],
// Daily site-wide limit before throttling begins
$settings['security']['global_daily_email_threshold'] = 300,
// Monthly site-wide limit before throttling
$settings['security']['global_monthly_email_threshold'] = 900,
Usage
In each action that involves sending an email, the function to perform the email security check has to be called before the mail is sent.
File: src/Domain/Authentication/Service/PasswordRecoveryEmailSender.php
public function sendPasswordRecoveryEmail(array $userValues): void
{
$this->userValidator->validatePasswordResetEmail($userValues);
// Verify that user doesn't spam email sending
$this->securityEmailChecker->performEmailAbuseCheck(
$userValues['email'],
$userValues['g-recaptcha-response'] ?? null
);
// ...
// Send email
$this->mailer->send($email);
// ...
}
The email request is logged to the database in the
mailer helper
send
function.
File: src/Infrastructure/Service/Mailer.php
public function send(Email $email): void
{
$this->mailer->send($email);
// Log email request
$this->emailLoggerRepository->logEmailRequest(
$email->getFrom()[0]->getAddress(),
$email->getTo()[0]->getAddress(),
$email->getSubject() ?? '',
$this->loggedInUserId
);
}
The reason the send
function not also performs the email security check is that if a
site-wide throttling is applied the captcha response is needed to be able to send the email,
and it'd be cumbersome to pass it from the action class all the way to the send
function each time.
Testing
To ensure that the login and email security checks work as expected and the correct throttling is applied,
the SecurityLoginChecker
and SecurityEmailChecker
are unit and integration
tested with each threshold for individual and global requests.
References
- https://paragonie.com/blog/2017/12/2018-guide-building-secure-php-software
- https://owasp.org/www-project-top-ten/
- https://phptherightway.com/#security
- https://paragonie.com/blog/2015/08/gentle-introduction-application-security
- https://phpsecurity.readthedocs.io/
- https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication
- https://stackoverflow.com/questions/2090910/how-can-i-throttle-user-login-attempts-in-php
- https://stackoverflow.com/questions/479233/what-is-the-best-distributed-brute-force-countermeasure