Security
Overview
Security is a big deal when it comes to web applications. You want to make sure that your application is secure and that your users' data is safe. Flight provides a number of features to help you secure your web applications.
Understanding
There are a number of common security threats that you should be aware of when building web applications. Some of the most common threats include:
- Cross Site Request Forgery (CSRF)
- Cross Site Scripting (XSS)
- SQL Injection
- Cross Origin Resource Sharing (CORS)
Templates help with XSS by escaping output by default so you don't have to remember to do that. Sessions can help with CSRF by storing a CSRF token in the user's session as outlined below. Using prepared statements with PDO can help prevent SQL injection attacks (or using handy methods in the PdoWrapper class). CORS can be handled with a simple hook before Flight::start()
is called.
All of these methods work together to help keep your web applications secure. It should always be at the forefront of your mind to learn and understand security best practices.
Basic Usage
Headers
HTTP headers are one of the easiest ways to secure your web applications. You can use headers to prevent clickjacking, XSS, and other attacks. There are several ways that you can add these headers to your application.
Two great websites to check for the security of your headers are securityheaders.com and observatory.mozilla.org. After you setup the below code, you can easily verify that your headers are working with those two websites.
Add By Hand
You can manually add these headers by using the header
method on the Flight\Response
object.
// Set the X-Frame-Options header to prevent clickjacking
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
// Set the Content-Security-Policy header to prevent XSS
// Note: this header can get very complex, so you'll want
// to consult examples on the internet for your application
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
// Set the X-XSS-Protection header to prevent XSS
Flight::response()->header('X-XSS-Protection', '1; mode=block');
// Set the X-Content-Type-Options header to prevent MIME sniffing
Flight::response()->header('X-Content-Type-Options', 'nosniff');
// Set the Referrer-Policy header to control how much referrer information is sent
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
// Set the Strict-Transport-Security header to force HTTPS
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
// Set the Permissions-Policy header to control what features and APIs can be used
Flight::response()->header('Permissions-Policy', 'geolocation=()');
These can be added at the top of your routes.php
or index.php
files.
Add as a Filter
You can also add them in a filter/hook like the following:
// Add the headers in a filter
Flight::before('start', function() {
Flight::response()->header('X-Frame-Options', 'SAMEORIGIN');
Flight::response()->header("Content-Security-Policy", "default-src 'self'");
Flight::response()->header('X-XSS-Protection', '1; mode=block');
Flight::response()->header('X-Content-Type-Options', 'nosniff');
Flight::response()->header('Referrer-Policy', 'no-referrer-when-downgrade');
Flight::response()->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
Flight::response()->header('Permissions-Policy', 'geolocation=()');
});
Add as a Middleware
You can also add them as a middleware class which provides the greatest flexibility for which routes to apply this to. In general, these headers should be applied to all HTML and API responses.
// app/middlewares/SecurityHeadersMiddleware.php
namespace app\middlewares;
use flight\Engine;
class SecurityHeadersMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
$response = $this->app->response();
$response->header('X-Frame-Options', 'SAMEORIGIN');
$response->header("Content-Security-Policy", "default-src 'self'");
$response->header('X-XSS-Protection', '1; mode=block');
$response->header('X-Content-Type-Options', 'nosniff');
$response->header('Referrer-Policy', 'no-referrer-when-downgrade');
$response->header('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
$response->header('Permissions-Policy', 'geolocation=()');
}
}
// index.php or wherever you have your routes
// FYI, this empty string group acts as a global middleware for
// all routes. Of course you could do the same thing and just add
// this only to specific routes.
Flight::group('', function(Router $router) {
$router->get('/users', [ 'UserController', 'getUsers' ]);
// more routes
}, [ SecurityHeadersMiddleware::class ]);
Cross Site Request Forgery (CSRF)
Cross Site Request Forgery (CSRF) is a type of attack where a malicious website can make a user's browser send a request to your website. This can be used to perform actions on your website without the user's knowledge. Flight does not provide a built-in CSRF protection mechanism, but you can easily implement your own by using middleware.
Setup
First you need to generate a CSRF token and store it in the user's session. You can then use this token in your forms and check it when the form is submitted. We'll use the flightphp/session plugin to manage sessions.
// Generate a CSRF token and store it in the user's session
// (assuming you've created a session object at attached it to Flight)
// see the session documentation for more information
Flight::register('session', flight\Session::class);
// You only need to generate a single token per session (so it works
// across multiple tabs and requests for the same user)
if(Flight::session()->get('csrf_token') === null) {
Flight::session()->set('csrf_token', bin2hex(random_bytes(32)) );
}
Using the default PHP Flight Template
<!-- Use the CSRF token in your form -->
<form method="post">
<input type="hidden" name="csrf_token" value="<?= Flight::session()->get('csrf_token') ?>">
<!-- other form fields -->
</form>
Using Latte
You can also set a custom function to output the CSRF token in your Latte templates.
Flight::map('render', function(string $template, array $data, ?string $block): void {
$latte = new Latte\Engine;
// other configurations...
// Set a custom function to output the CSRF token
$latte->addFunction('csrf', function() {
$csrfToken = Flight::session()->get('csrf_token');
return new \Latte\Runtime\Html('<input type="hidden" name="csrf_token" value="' . $csrfToken . '">');
});
$latte->render($finalPath, $data, $block);
});
And now in your Latte templates you can use the csrf()
function to output the CSRF token.
<form method="post">
{csrf()}
<!-- other form fields -->
</form>
Check the CSRF Token
You can check the CSRF token using several methods.
Middleware
// app/middlewares/CsrfMiddleware.php
namespace app\middleware;
use flight\Engine;
class CsrfMiddleware
{
protected Engine $app;
public function __construct(Engine $app)
{
$this->app = $app;
}
public function before(array $params): void
{
if($this->app->request()->method == 'POST') {
$token = $this->app->request()->data->csrf_token;
if($token !== $this->app->session()->get('csrf_token')) {
$this->app->halt(403, 'Invalid CSRF token');
}
}
}
}
// index.php or wherever you have your routes
use app\middlewares\CsrfMiddleware;
Flight::group('', function(Router $router) {
$router->get('/users', [ 'UserController', 'getUsers' ]);
// more routes
}, [ CsrfMiddleware::class ]);
Event Filters
// This middleware checks if the request is a POST request and if it is, it checks if the CSRF token is valid
Flight::before('start', function() {
if(Flight::request()->method == 'POST') {
// capture the csrf token from the form values
$token = Flight::request()->data->csrf_token;
if($token !== Flight::session()->get('csrf_token')) {
Flight::halt(403, 'Invalid CSRF token');
// or for a JSON response
Flight::jsonHalt(['error' => 'Invalid CSRF token'], 403);
}
}
});
Cross Site Scripting (XSS)
Cross Site Scripting (XSS) is a type of attack where a malicious form input can inject code into your website. Most of these opportunities come from form values that your end users will fill out. You should never trust output from your users! Always assume all of them are the best hackers in the world. They can inject malicious JavaScript or HTML into your page. This code can be used to steal information from your users or perform actions on your website. Using Flight's view class or another templating engine like Latte, you can easily escape output to prevent XSS attacks.
// Let's assume the user is clever as tries to use this as their name
$name = '<script>alert("XSS")</script>';
// This will escape the output
Flight::view()->set('name', $name);
// This will output: <script>alert("XSS")</script>
// If you use something like Latte registered as your view class, it will also auto escape this.
Flight::view()->render('template', ['name' => $name]);
SQL Injection
SQL Injection is a type of attack where a malicious user can inject SQL code into your database. This can be used to steal information
from your database or perform actions on your database. Again you should never trust input from your users! Always assume they are
out for blood. You can use prepared statements in your PDO
objects will prevent SQL injection.
// Assuming you have Flight::db() registered as your PDO object
$statement = Flight::db()->prepare('SELECT * FROM users WHERE username = :username');
$statement->execute([':username' => $username]);
$users = $statement->fetchAll();
// If you use the PdoWrapper class, this can easily be done in one line
$users = Flight::db()->fetchAll('SELECT * FROM users WHERE username = :username', [ 'username' => $username ]);
// You can do the same thing with a PDO object with ? placeholders
$statement = Flight::db()->fetchAll('SELECT * FROM users WHERE username = ?', [ $username ]);
Insecure Example
The below is why we use SQL prepared statements to protect from innocent examples like the below:
// end user fills out a web form.
// for the value of the form, the hacker puts in something like this:
$username = "' OR 1=1; -- ";
$sql = "SELECT * FROM users WHERE username = '$username' LIMIT 5";
$users = Flight::db()->fetchAll($sql);
// After the query is build it looks like this
// SELECT * FROM users WHERE username = '' OR 1=1; -- LIMIT 5
// It looks strange, but it's a valid query that will work. In fact,
// it's a very common SQL injection attack that will return all users.
var_dump($users); // this will dump all users in the database, not just the one single username
CORS
Cross-Origin Resource Sharing (CORS) is a mechanism that allows many resources (e.g., fonts, JavaScript, etc.) on a web page to be
requested from another domain outside the domain from which the resource originated. Flight does not have built in functionality,
but this can easily be handled with a hook to run before the Flight::start()
method is called.
// app/utils/CorsUtil.php
namespace app\utils;
class CorsUtil
{
public function set(array $params): void
{
$request = Flight::request();
$response = Flight::response();
if ($request->getVar('HTTP_ORIGIN') !== '') {
$this->allowOrigins();
$response->header('Access-Control-Allow-Credentials', 'true');
$response->header('Access-Control-Max-Age', '86400');
}
if ($request->method === 'OPTIONS') {
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_METHOD') !== '') {
$response->header(
'Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
);
}
if ($request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS') !== '') {
$response->header(
"Access-Control-Allow-Headers",
$request->getVar('HTTP_ACCESS_CONTROL_REQUEST_HEADERS')
);
}
$response->status(200);
$response->send();
exit;
}
}
private function allowOrigins(): void
{
// customize your allowed hosts here.
$allowed = [
'capacitor://localhost',
'ionic://localhost',
'http://localhost',
'http://localhost:4200',
'http://localhost:8080',
'http://localhost:8100',
];
$request = Flight::request();
if (in_array($request->getVar('HTTP_ORIGIN'), $allowed, true) === true) {
$response = Flight::response();
$response->header("Access-Control-Allow-Origin", $request->getVar('HTTP_ORIGIN'));
}
}
}
// index.php or wherever you have your routes
$CorsUtil = new CorsUtil();
// This needs to be run before start runs.
Flight::before('start', [ $CorsUtil, 'setupCors' ]);
Error Handling
Hide sensitive error details in production to avoid leaking info to attackers. On production, log errors instead of displaying them with display_errors
set to 0
.
// In your bootstrap.php or index.php
// add this to your app/config/config.php
$environment = ENVIRONMENT;
if ($environment === 'production') {
ini_set('display_errors', 0); // Disable error display
ini_set('log_errors', 1); // Log errors instead
ini_set('error_log', '/path/to/error.log');
}
// In your routes or controllers
// Use Flight::halt() for controlled error responses
Flight::halt(403, 'Access denied');
Input Sanitization
Never trust user input. Sanitize it using filter_var before processing to prevent malicious data from sneaking in.
// Lets assume a $_POST request with $_POST['input'] and $_POST['email']
// Sanitize a string input
$clean_input = filter_var(Flight::request()->data->input, FILTER_SANITIZE_STRING);
// Sanitize an email
$clean_email = filter_var(Flight::request()->data->email, FILTER_SANITIZE_EMAIL);
Password Hashing
Store passwords securely and verify them safely using PHP’s built-in functions like password_hash and password_verify. Passwords should never be stored in plain text, nor should they be encrypted with reversible methods. Hashing ensures that even if your database is compromised, the actual passwords remain protected.
$password = Flight::request()->data->password;
// Hash a password when storing (e.g., during registration)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
// Verify a password (e.g., during login)
if (password_verify($password, $stored_hash)) {
// Password matches
}
Rate Limiting
Protect against brute force attacks or denial-of-service attacks by limiting request rates with a cache.
// Assuming you have flightphp/cache installed and registered
// Using flightphp/cache in a filter
Flight::before('start', function() {
$cache = Flight::cache();
$ip = Flight::request()->ip;
$key = "rate_limit_{$ip}";
$attempts = (int) $cache->retrieve($key);
if ($attempts >= 10) {
Flight::halt(429, 'Too many requests');
}
$cache->set($key, $attempts + 1, 60); // Reset after 60 seconds
});
See Also
- Sessions - How to manage user sessions securely.
- Templates - Using templates to auto-escape output and prevent XSS.
- PDO Wrapper - Simplified database interactions with prepared statements.
- Middleware - How to use middleware for simplifying the process of adding security headers.
- Responses - How to customize HTTP responses with secure headers.
- Requests - How to handle and sanitize user input.
- filter_var - PHP function for input sanitization.
- password_hash - PHP function for secure password hashing.
- password_verify - PHP function for verifying hashed passwords.
Troubleshooting
- Refer to the "See Also" section above for troubleshooting information related to issues with components of the Flight Framework.
Changelog
- v3.1.0 - Added sections on CORS, Error Handling, Input Sanitization, Password Hashing, and Rate Limiting.
- v2.0 - Added escaping for default views to prevent XSS.