NightFox

NightFox

PHP Application Security

There's no magic make_secure() function. Application security is a bunch of habits layered together: how you handle input and output, logins and permissions, config, dependencies, and day-to-day operations. No firewall will make up for an app that skipped them. These are the ones I don't compromise on.

Prevent SQL and command injection

Prepared statements for values, a strict allow-list for things like column names (which can't be bound), and no shelling out when there's a PHP function or library that does the job. If you really have to run a command, escapeshellarg() every argument.

<?php
declare(strict_types=1);

$stmt = $pdo->prepare('SELECT id FROM users WHERE email = :email');
$stmt->execute(['email' => $email]);

// When shell execution is unavoidable, pass each argument safely.
$command = sprintf(
    'ping -c 1 -- %s',
    escapeshellarg($validatedIpAddress)
);

Escape for the output context

<p><?= htmlspecialchars($customerName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></p>

Don't drop untrusted values straight into inline JavaScript, CSS, or raw HTML. Each of those has its own escaping rules and it's easy to get wrong. I put values in HTML-escaped data attributes and read them from an external script instead.

Protect state-changing browser requests

<?php
declare(strict_types=1);

$_SESSION['csrf_token'] ??= bin2hex(random_bytes(32));

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $submitted = $_POST['csrf_token'] ?? '';

    if (!hash_equals($_SESSION['csrf_token'], $submitted)) {
        http_response_code(419);
        exit('The form expired. Reload the page and try again.');
    }
}

Protect authentication and authorization

  • Use password_hash() and password_verify().
  • Regenerate the session ID after login.
  • Rate-limit authentication attempts.
  • Require re-authentication for high-risk changes.
  • Use role or permission checks at the server-side action boundary.
  • Record privileged actions in an audit log.

Treat file uploads as hostile

<?php
declare(strict_types=1);

$file = $_FILES['document'] ?? null;

if (!is_array($file) || $file['error'] !== UPLOAD_ERR_OK) {
    throw new RuntimeException('Upload failed.');
}

if ($file['size'] > 5 * 1024 * 1024) {
    throw new RuntimeException('File exceeds the 5 MB limit.');
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowed = ['application/pdf' => 'pdf', 'image/jpeg' => 'jpg'];

if (!isset($allowed[$mime])) {
    throw new RuntimeException('Unsupported file type.');
}

$filename = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
$destination = dirname(__DIR__, 2) . '/storage/uploads/' . $filename;

if (!move_uploaded_file($file['tmp_name'], $destination)) {
    throw new RuntimeException('Unable to store uploaded file.');
}

Configuration and secrets

  • Keep secrets out of source control and the public document root.
  • Use separate credentials for development, staging, and production.
  • Give the database account only the privileges the application needs.
  • Rotate credentials after exposure and remove them from history.
  • Never put secrets in URLs, client-side JavaScript, or logs.

Useful response headers

<?php
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Permissions-Policy: camera=(), microphone=(), geolocation=()');
header("Content-Security-Policy: default-src 'self'; frame-ancestors 'none'; base-uri 'self'");

Build your Content Security Policy around what the app actually loads. On an existing site, run it in report-only mode for a while first. You'll be surprised what's in there.

Log security-relevant events

Log logins (good and bad), permission denials, password changes, user and role changes, new tokens, data exports, and admin actions. Each entry should say who did it, to what, when, the request ID, and whether it worked. What should never end up in the log: passwords, session IDs, API keys, full card numbers, and personal info you don't need.

Last Modified: October 12, 2026, 11:46 am