Web Security

PHP Security Basics for Small Websites

Prepared statements, output escaping, password hashing, sessions, uploads, secrets and backups, with code you can adapt.

Most small-website breaches are not clever attacks. They are automated scripts trying known weaknesses: a form that builds SQL from user input, a forgotten admin password, an outdated plugin, a backup file left in the web root. The good news is that a handful of habits closes the large majority of these doors. This guide walks through the basics for PHP sites, with code you can adapt, in the order that gives the most protection for the least effort.

Think like the attacker: where does input come from?

Almost every PHP vulnerability starts with data you did not write: form fields, query strings, cookies, headers, uploaded files and even data from your own database that originally came from a user. The rule is simple: validate input when it arrives, and escape output when it leaves. Never trust either side to be safe.

Stop SQL injection with prepared statements

SQL injection happens when user input is concatenated into a query string. An attacker can then change what the query does, for example reading every user's data or bypassing a login. The fix is to keep the query and the data separate using prepared statements.

// Unsafe: user input is part of the SQL text
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";

// Safe: PDO prepared statement
$pdo  = new PDO($dsn, $user, $pass, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$_POST['email']]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Use placeholders for every value. If you must choose a table or column name dynamically, never insert user input directly; compare it against a fixed allow-list instead.

Prevent cross-site scripting by escaping output

Cross-site scripting (XSS) lets an attacker run JavaScript in your visitors' browsers by getting your page to print their input as HTML. Escape every dynamic value at the point you output it.

function e(?string $s): string {
    return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8');
}

echo '<p>Hello, ' . e($_GET['name'] ?? '') . '</p>';

Escaping depends on context: HTML text, HTML attributes, URLs and JavaScript each need different handling. For values placed in JavaScript, use json_encode(). A Content-Security-Policy header adds a second layer of defence, though it is not a substitute for escaping.

Store passwords correctly

Never store passwords in plain text, and never use fast hashes such as MD5 or SHA-1 for them. PHP has built-in functions that handle salting and cost for you.

// When creating or changing a password
$hash = password_hash($password, PASSWORD_DEFAULT);

// When logging in
if ($row && password_verify($password, $row['password_hash'])) {
    session_regenerate_id(true);   // new session ID after login
    $_SESSION['user_id'] = $row['id'];
}

// Optionally upgrade old hashes
if (password_needs_rehash($row['password_hash'], PASSWORD_DEFAULT)) { /* store a new hash */ }

Never leave a default admin password in place. If your project ships with a sample password, change it before the site goes live and ideally refuse to run while the default is still set.

Secure sessions and logins

  • Cookie flags: set HttpOnly, Secure (on HTTPS) and SameSite=Lax or Strict on session cookies.
  • Regenerate the session ID after login and privilege changes to prevent session fixation.
  • Set timeouts: expire idle admin sessions.
  • Limit login attempts: slow down or temporarily block repeated failures from the same IP or account.
  • Log out properly: clear session data and destroy the session.
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', '1');
if (!empty($_SERVER['HTTPS'])) { ini_set('session.cookie_secure', '1'); }
session_start();

Protect forms with CSRF tokens

Cross-site request forgery tricks a logged-in user's browser into sending a request to your site, such as deleting a record. Add a random token to every state-changing form and check it on the server.

// Generate once per session
if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); }

// In the form
echo '<input type="hidden" name="csrf" value="' . e($_SESSION['csrf']) . '">';

// On submit
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(403); exit('Invalid request');
}

Handle file uploads with suspicion

Uploads are a classic route to a compromised server, because an attacker may try to upload a script and then run it.

  • Check the real file type on the server with finfo, not just the extension or the browser-supplied type.
  • Allow only the extensions you need (for example jpg, png, webp, pdf).
  • Generate your own file names; never use the original name directly.
  • Set a maximum size, and store uploads outside the web root or in a folder where PHP execution is disabled.
  • Re-process images (resize or re-encode) where possible, which also strips unexpected content.

Keep secrets out of your code and web root

Database passwords, API keys and SMTP credentials should come from environment variables or a configuration file stored outside the public folder, never committed to a public repository.

  • Store databases (such as SQLite files) above the web root so they cannot be downloaded.
  • Block direct access to sensitive folders and files (includes/, .env, *.sql, backups, .git) in .htaccess.
  • Turn off error display in production (display_errors = Off) and log errors to a file instead, so stack traces never reach visitors.
# .htaccess
RewriteEngine On
RewriteRule ^(includes|data|\.git)/ - [F,L]
<FilesMatch "\.(env|sql|bak|md|log)$">
  Require all denied
</FilesMatch>

Send basic security headers

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=()');

Serve the whole site over HTTPS and redirect HTTP to HTTPS. Consider HSTS once you are sure HTTPS works everywhere.

Updates, backups and monitoring

  • Update everything: PHP, CMS, plugins, themes and libraries. Most real-world compromises exploit known, already-patched vulnerabilities. Remove plugins you do not use.
  • Back up regularly and keep copies off the server. Test a restore at least once, because an untested backup is only a hope.
  • Review accounts: remove old admin users and use unique, long passwords or a password manager.
  • Watch for signs of trouble: unexpected files, new admin users, sudden traffic spikes and spam emails sent from your domain.

A starter checklist

  1. All database access uses prepared statements.
  2. All output is escaped with htmlspecialchars or json_encode.
  3. Passwords use password_hash; no default credentials remain.
  4. Sessions use secure cookie flags and regenerate on login.
  5. State-changing forms have CSRF tokens.
  6. Uploads are validated, renamed and non-executable.
  7. Secrets and databases live outside the web root; errors are not displayed.
  8. The site runs on HTTPS with security headers.
  9. Software is current and backups are tested.

A note on scope

This is a baseline for small sites, not a full security audit. If your site handles payments, health or financial data, or sensitive personal information, have it reviewed by a qualified security professional.

Found a mistake or an outdated detail? Email contact@abtech.com.et and we will review it. See our Editorial Policy.