-
-
Notifications
You must be signed in to change notification settings - Fork 6
Security
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 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 (parameterized queries), cross-site scripting (escaping output), and brute force attacks (request throttling).
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 is delegated to the cakephp/validation
library.
The Validation chapter explains how the library is used.
Instead of directly including user input in the SQL statement, a placeholder
(like a question mark or a named parameter) is 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 library cakephp/database
for instance, automatically escapes parameters.
The Repository chapter explains how the database is accessed with this library.
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. The list goes on.
As protection, all output has to be escaped so that they're treated as plain text
by the browser and not 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 functionhtml()
.
File: functions.php
function html(?string $text = null): string
{
return htmlspecialchars($text ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
For JavaScript, the function escapeHtml()
serves the same purpose.
File: functions.js
export function escapeHtml(unsafeString) {
return unsafeString?.toString()
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
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.
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.
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;
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.
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 number 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.
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 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
$this->authenticationLogger->logLoginRequest($userLoginValues['email'], true);
// return the user id
// If unsuccessful, log request
$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);
}
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.
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 as login)
$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,
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.
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.
In each action that involves sending an email, the function to perform the email security check has to be called.
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 to the send
function.
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.
- 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
Slim app basics
- Composer
- Web Server config and Bootstrapping
- Dependency Injection
- Configuration
- Routing
- Middleware
- Architecture
- Single Responsibility Principle
- Action
- Domain
- Repository and Query Builder
Features
- Logging
- Validation
- Session and Flash
- Authentication
- Authorization
- Translations
- Mailing
- Console commands
- Database migrations
- Error handling
- Security
- API endpoint
- GitHub Actions
- Scrutinizer
- Coding standards fixer
- PHPStan static code analysis
Testing
Frontend
Other