Security Mindset
Two principles drive every defense:
- Never trust input. User input, API responses, file contents - validate everything at the boundary.
- Escape per context. Same data needs different escaping for HTML, URL, JS, SQL.
SQL Injection
<?php
// VULNERABLE
$pdo->query("SELECT * FROM users WHERE id = " . $_GET["id"]);
// SAFE - prepared statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET["id"]]);
// Identifiers cant be placeholders - validate against allow-list
$allowed = ["name", "email", "created_at"];
$sort = in_array($_GET["sort"] ?? "", $allowed, true) ? $_GET["sort"] : "id";
$pdo->query("SELECT * FROM users ORDER BY $sort");
Detailed: Prepared Statements tutorial.
XSS - Cross-Site Scripting
Untrusted data echoed into HTML lets attackers inject scripts that run in your users\' browsers - stealing cookies, taking over accounts, defacing pages.
<?php
// VULNERABLE
echo "<h1>Hello $name</h1>";
// SAFE - HTML context
echo "<h1>Hello " . htmlspecialchars($name, ENT_QUOTES, "UTF-8") . "</h1>";
// HTML attribute - same plus ENT_QUOTES (default for above)
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES) . '">';
// JavaScript string context - encode with json_encode
echo "<script>const user = " . json_encode($name,
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ";</script>";
// URL context
echo "<a href=\"/search?q=" . urlencode($q) . "\">";
CSP is a browser-enforced last line of defense. Content-Security-Policy: default-src 'self'; blocks inline scripts and external sources unless you opt in. Even if XSS slips through, CSP often blocks the payload.
CSRF - Cross-Site Request Forgery
An attacker tricks a logged-in user\'s browser into submitting requests to your site using its valid cookies. Defense: a per-session token your form includes that an attacker cannot guess.
<?php
session_start();
function csrf_token(): string {
return $_SESSION["csrf"] ??= bin2hex(random_bytes(32));
}
function csrf_check(): void {
if (!hash_equals($_SESSION["csrf"] ?? "", $_POST["csrf"] ?? "")) {
http_response_code(403);
exit("Invalid CSRF token");
}
}
// On form display
?>
<form method="post">
<input type="hidden" name="csrf" value="<?= csrf_token() ?>">
...
</form>
<?php
// On submission
if ($_SERVER["REQUEST_METHOD"] === "POST") {
csrf_check();
// ... safe to process ...
}
For APIs that use only the SameSite=Lax cookie attribute (modern browsers), CSRF risk on top-level navigations is largely mitigated - but token-based defense is still recommended for state-changing requests.
Password Hashing
<?php
// Registration - hash the password
$hash = password_hash($plain, PASSWORD_BCRYPT); // or PASSWORD_ARGON2ID
// Store $hash in the database (NOT $plain)
// Login - verify
if (password_verify($plain, $row["password_hash"])) {
// Granted
// Check if rehash needed (e.g., cost factor was upgraded)
if (password_needs_rehash($row["password_hash"], PASSWORD_BCRYPT)) {
$newHash = password_hash($plain, PASSWORD_BCRYPT);
// Update the stored hash
}
}
If your DB leaks, hashed passwords slow attackers down dramatically. MD5/SHA1 are cracked in seconds with rainbow tables - bcrypt/Argon2id take years.
Session Security
- Regenerate session ID on login:
session_regenerate_id(true) - HttpOnly cookies - JS cannot read the session cookie
- Secure cookies - sent only over HTTPS
- SameSite=Lax - CSRF protection by default
- Short lifetime for sensitive admin sessions
<?php
session_start([
"cookie_secure" => true,
"cookie_httponly" => true,
"cookie_samesite" => "Lax",
"use_strict_mode" => true,
]);
File Upload Security
<?php
$file = $_FILES["upload"] ?? null;
if (!$file || $file["error"] !== UPLOAD_ERR_OK) die("Upload failed");
// Size limit
if ($file["size"] > 5 * 1024 * 1024) die("Too large");
// MIME by CONTENT, not extension
$mime = mime_content_type($file["tmp_name"]);
$allowed = ["image/jpeg" => "jpg", "image/png" => "png"];
if (!isset($allowed[$mime])) die("Type not allowed");
// Generate random filename - never use user-supplied name
$name = bin2hex(random_bytes(16)) . "." . $allowed[$mime];
// Store OUTSIDE the web root if possible, or in a folder with PHP execution disabled
$dest = __DIR__ . "/../private-uploads/$name";
if (!move_uploaded_file($file["tmp_name"], $dest)) die("Save failed");
Place this in any upload folder\'s .htaccess to neutralize uploaded executables:
php_flag engine off
<FilesMatch "\.(php|phtml|phar|pl|py|cgi|sh)$">
Require all denied
</FilesMatch>
Secure Headers
<?php
header("X-Content-Type-Options: nosniff");
header("X-Frame-Options: SAMEORIGIN");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Permissions-Policy: camera=(), microphone=(), geolocation=()");
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// Once HTTPS is verified working
header("Strict-Transport-Security: max-age=31536000; includeSubDomains");
Environment & Secrets
- Never commit secrets to git. Use
.envfiles loaded with phpdotenv. - Add
.envto.gitignoreand to.htaccessdeny rules. - Read secrets from environment variables at runtime - never hardcode.
- Use a secrets manager for production (AWS Secrets Manager, HashiCorp Vault).
Supply Chain (composer audit)
# Check for known vulnerabilities in dependencies
composer audit
# Also in CI - fail the build on findings
composer audit --format=json | jq '.vulnerabilities | length'
# Keep deps up to date
composer outdated --direct
Production Checklist
- [ ] Running supported PHP version with latest security patches
- [ ]
display_errors = Offin production php.ini - [ ] HTTPS enforced (HSTS header)
- [ ] All forms have CSRF tokens
- [ ] All output through
htmlspecialchars()(or templating engine) - [ ] All SQL uses prepared statements
- [ ] Passwords stored with
password_hash() - [ ] Session cookies: secure, httponly, samesite
- [ ] Secure headers set (CSP, X-Frame-Options, etc.)
- [ ] Uploads validated by MIME content, stored with random names
- [ ] Secrets in environment variables, not in code
- [ ]
composer auditclean - [ ] Rate limiting on auth endpoints
- [ ] Web server hardened (no directory listings, no exposed .git/.env)
- [ ] Logging and monitoring in place