Skip to main content
Back to blog
April 28, 2026·Updated April 28, 2026|8 min read|Antoine Duno

PHP Security Best Practices 2026: PDO, XSS, Sessions & More

PHP powers over 77% of the web, making php security best practices essential knowledge for every developer. This guide covers the most critical vulnerabilities and how to fix them.

ZeriFlow Team

1,118 words

PHP Security Best Practices 2026: PDO, XSS, Sessions & More

PHP security best practices are non-negotiable in 2026 — PHP still powers over 77% of the web, making it the single largest attack surface on the internet. Whether you are running a legacy application or building fresh with PHP 8.3, this guide gives you actionable, code-level techniques to harden your stack immediately.

Before we dive in, run a free automated scan on your site with ZeriFlow to get an instant baseline of your current security posture across 80+ checks.


1. Use PDO Prepared Statements to Prevent SQL Injection

SQL injection remains the #1 vulnerability in PHP applications. The fix is always the same: never interpolate user input directly into SQL. Use PDO with prepared statements and bound parameters.

Vulnerable code:

php
$query = 'SELECT * FROM users WHERE email = ' . $_POST['email'];
$result = $pdo->query($query);

Secure code:

php
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email');
$stmt->execute([':email' => $_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Additional PDO hardening:

php
$pdo = new PDO($dsn, $user, $pass, [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false, // use native prepares
]);

Setting ATTR_EMULATE_PREPARES to false forces the database driver to handle parameterization natively, closing edge-case bypass vectors that exist in emulated mode.


2. Escape Output to Prevent XSS

Cross-site scripting (XSS) attacks inject malicious scripts into pages viewed by other users. The root cause is always the same: trusting user-controlled data at render time.

Always use `htmlspecialchars()` with the correct flags:

php
// Unsafe
echo $_GET['name'];

// Safe
echo htmlspecialchars($_GET['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8');

For HTML attributes, use the same approach. For JavaScript contexts, use json_encode():

php
<script>
  var username = <?= json_encode($username, JSON_HEX_TAG | JSON_HEX_AMP) ?>;
</script>

For rich text input where users legitimately need HTML, use a whitelist library like HTML Purifier rather than trying to sanitize manually:

php
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);

3. Harden PHP Session Security

Sessions are a prime target. Default PHP session configuration leaves several attack surfaces open.

Start with a secure session configuration:

php
ini_set('session.cookie_httponly', 1);     // Prevent JS access
ini_set('session.cookie_secure', 1);       // HTTPS only
ini_set('session.cookie_samesite', 'Lax'); // CSRF mitigation
ini_set('session.use_strict_mode', 1);     // Reject uninitialized session IDs
ini_set('session.gc_maxlifetime', 1800);   // 30-minute idle timeout
session_start();

Regenerate the session ID on privilege escalation:

php
// Always call this after login or role change
session_regenerate_id(true); // true = delete old session
$_SESSION['user_id'] = $authenticated_user_id;
$_SESSION['role']    = $user_role;

Destroy sessions completely on logout:

php
$_SESSION = [];
if (ini_get('session.use_cookies')) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000,
        $params['path'], $params['domain'],
        $params['secure'], $params['httponly']
    );
}
session_destroy();

4. Harden php.ini for Production

The default php.ini is optimized for developer convenience, not production security. These settings should be locked down before any public deployment.

ini
; Disable dangerous functions
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,
                    curl_exec,curl_multi_exec,parse_ini_file,show_source

; Hide PHP version from headers
expose_php = Off

; Prevent remote file inclusion
allow_url_include = Off
allow_url_fopen   = Off

; Limit error output in production
display_errors   = Off
log_errors       = On
error_log        = /var/log/php_errors.log

; Restrict file uploads
file_uploads    = On
upload_max_filesize = 2M
max_file_uploads    = 5

; Session hardening (already shown above, also set here)
session.cookie_httponly = 1
session.cookie_secure   = 1
session.use_strict_mode = 1

Reload PHP-FPM after changes: sudo systemctl reload php8.3-fpm


5. Implement CSRF Protection

Cross-site request forgery tricks authenticated users into performing unwanted actions. The mitigation is a synchronizer token pattern.

php
// Generate token at session start
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// Embed in every form
echo '<input type="hidden" name="csrf_token" value="'
     . htmlspecialchars($_SESSION['csrf_token']) . '">';

// Validate on submission
function validate_csrf(string $token): bool {
    return isset($_SESSION['csrf_token'])
        && hash_equals($_SESSION['csrf_token'], $token);
}

if (!validate_csrf($_POST['csrf_token'] ?? '')) {
    http_response_code(403);
    exit('Invalid CSRF token');
}

Note: hash_equals() is timing-attack safe — never use == for token comparison.


6. Set Security Headers in PHP

HTTP security headers are a fast, lightweight defense layer. Set them early in your bootstrap file or via .htaccess.

php
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
header("Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'");
header('Permissions-Policy: geolocation=(), microphone=(), camera=()');

// Only over HTTPS
if (isset($_SERVER['HTTPS'])) {
    header('Strict-Transport-Security: max-age=31536000; includeSubDomains; preload');
}

Scan your headers for free on ZeriFlow — it checks all major headers in seconds.


FAQ

### Q: What is the most common PHP security vulnerability in 2026? A: SQL injection and XSS remain the top two. Both are trivially preventable with PDO prepared statements and htmlspecialchars() output escaping respectively. Tools like ZeriFlow can automatically detect missing protections before attackers do.

### Q: Should I use mysqli or PDO? A: PDO is strongly preferred. It supports multiple database backends, enforces prepared statements more strictly when ATTR_EMULATE_PREPARES is false, and has a cleaner API. Avoid raw mysql_* functions — they have been removed since PHP 7.

### Q: How do I store passwords securely in PHP? A: Always use password_hash($password, PASSWORD_ARGON2ID) and verify with password_verify(). Never use MD5, SHA1, or unsalted hashes. Argon2id is the recommended algorithm for new applications in 2026.

### Q: Is open_basedir worth setting? A: Yes. Setting open_basedir to your application's root directory prevents PHP from reading arbitrary files on the server, limiting damage from path traversal vulnerabilities.

### Q: How often should I audit PHP dependencies? A: Run composer audit in your CI pipeline on every push. It checks your composer.lock against the PHP Security Advisories Database and fails the build on known CVEs.


Conclusion

PHP security best practices in 2026 come down to a handful of disciplines: parameterized queries, consistent output escaping, hardened session management, locked-down php.ini, and comprehensive HTTP security headers. None of these require expensive tooling — just discipline and the right defaults.

The fastest way to know where your application stands right now is an automated scan. Run a free ZeriFlow security scan and get a full report across 80+ checks in under a minute. Fix the gaps, commit the changes, and scan again — make it part of your deployment pipeline today.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading