Skip to content

Security

Samuel Gfeller edited this page Dec 21, 2023 · 12 revisions

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. Input validation is 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 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.

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.

Every value rendered with the template renderer has to be escaped with the autoloaded global function html().

<?= html($client->firstName) ?>

File: functions.php

function html(?string $text = null): string
{
    return htmlspecialchars($text ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

The JavaScript function escapeHtml() serves the same purpose.

File: functions.js

export function escapeHtml(unsafeString) {
    return unsafeString?.toString()
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
}

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 employed 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 numerous combinations, often with the help of 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 throttling is implemented in the src/Domain/Security namespace.

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;

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 failed login requests in the past $settings['security']['timespan'] seconds.

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 in $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 were 200 login requests, the threshold is reached when there are more than 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 same limit of login attempts is enforced for individual successful logins.

Implementation

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 holding the remaining delay.

Failed and successful authentication requests are logged in the database to make the future security checks.

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.

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,

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 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.

Implementation

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 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.

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

Clone this wiki locally