Online admissions open for the SEO, PHP web development and networking & cybersecurity courses. Enquire for the next batch.

PHP Security: Preventing SQL Injection, XSS and CSRF

6 min read Updated By the SkillNewton team

What does PHP security involve?

PHP security is the practice of writing and running PHP applications so attackers cannot read, change or destroy data they should not reach. In practice it means using prepared statements against SQL injection, escaping output against cross-site scripting, checking CSRF tokens on forms, hashing passwords properly, validating uploads and configuring the server so errors and secrets are never exposed.

Key threats and the terms behind them

Most attacks on PHP sites exploit a small number of well-known weaknesses. The OWASP Top 10, a widely used awareness list of web application security risks, is a good reference for the wider picture. These are the ones every PHP developer must handle:

  • SQL injection: user input changes the meaning of a database query.
  • Cross-site scripting (XSS): user input is output as HTML or JavaScript and runs in other visitors' browsers.
  • Cross-site request forgery (CSRF): another site makes a logged-in user's browser submit a form to your site.
  • Weak password storage: passwords saved in plain text or with fast hashes such as MD5 can be recovered after a leak.
  • Unsafe uploads: an uploaded file turns out to be a script the server will execute.

The examples here are defensive. Test them only on applications you own or are authorised to test. For the network side of defence, see our cybersecurity basics guide.

How prepared statements stop SQL injection

Building SQL by joining strings lets input such as ' OR '1'='1 rewrite your query. A prepared statement sends the query structure and the data separately, so input is always treated as a value, never as SQL.

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=shop;charset=utf8mb4',
    'shop_app',
    getenv('DB_PASSWORD'),
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

// Unsafe: "SELECT * FROM users WHERE email = '$email'"
$stmt = $pdo->prepare('SELECT id, password_hash FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email'] ?? '']);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

Placeholders work for values only. Table names, column names and sort directions cannot be bound, so check them against an allow-list such as in_array($sort, ['name', 'price'], true). The MySQL fundamentals guide explains the queries themselves.

Escaping output against XSS and checking CSRF tokens

Escape every value at the moment you print it into HTML. Pass the flags and encoding explicitly so behaviour does not depend on configuration.

<?php
function e(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

// Create one token per session
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<p>Hello, <?= e($user['name']) ?></p>
<form method="post" action="/profile">
  <input type="hidden" name="csrf_token" value="<?= e($_SESSION['csrf_token']) ?>">
  <button>Save</button>
</form>
<?php
// When the form is submitted
$sent = (string) ($_POST['csrf_token'] ?? '');
if (!hash_equals($_SESSION['csrf_token'], $sent)) {
    http_response_code(403);
    exit('Invalid request');
}

ENT_QUOTES escapes both single and double quotes, which protects attribute values. htmlspecialchars() is for HTML only: use rawurlencode() inside URLs and json_encode() with JSON_HEX_TAG when passing data into JavaScript. hash_equals() compares in constant time, so timing differences do not leak the token. Pair tokens with SameSite cookies as described in the sessions and cookies guide.

Passwords, validation and file uploads

Store passwords with password_hash($password, PASSWORD_DEFAULT) in a VARCHAR(255) column and check them with password_verify(). The function creates a random salt and a slow hash for you. Call password_needs_rehash() after a successful login so old hashes upgrade when PHP changes its default.

Validation and escaping are different jobs. Validate input when it arrives: is this a valid email, a whole number in range, one of the allowed options? Escape output when it leaves, according to where it goes. Validation does not make data safe to print, and escaping does not make data correct.

<?php
$qty = filter_var($_POST['qty'] ?? '', FILTER_VALIDATE_INT,
    ['options' => ['min_range' => 1, 'max_range' => 10]]);
if ($qty === false) { /* show an error */ }

$file = $_FILES['photo'] ?? null;
if ($file && $file['error'] === UPLOAD_ERR_OK && $file['size'] <= 2_000_000) {
    $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']);
    $allowed = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
    if (isset($allowed[$mime])) {
        $name = bin2hex(random_bytes(16)) . '.' . $allowed[$mime];
        move_uploaded_file($file['tmp_name'], '/var/app/uploads/' . $name);
    }
}

Never trust the browser-supplied file name or type. Detect the type from the file contents, generate a new name, and store uploads outside the public web root or in a folder where the server will not execute scripts.

Securing a PHP application step by step

  1. Replace every query built with string joining by a PDO prepared statement.
  2. Route all output through an escaping helper or a template engine that escapes by default.
  3. Add a CSRF token to every form that changes data and reject requests without a valid one.
  4. Move passwords to password_hash() and rehash old ones on next login.
  5. In production php.ini, set display_errors = Off, log_errors = On and keep error_reporting = E_ALL, so errors are logged but never shown.
  6. Send security headers such as Content-Security-Policy, X-Content-Type-Options: nosniff, Referrer-Policy and, on HTTPS sites, Strict-Transport-Security.
  7. Run composer audit to check dependencies for known vulnerabilities, update regularly, and stay on a PHP version that still receives security fixes.
  8. Connect with a database user that has only the rights the application needs, never the root account.

Security headers can be sent from PHP before any output, or set once in the web server configuration. A starting set looks like this; tighten the Content-Security-Policy to match the scripts and styles your site actually loads.

<?php
header("Content-Security-Policy: default-src 'self'; frame-ancestors 'none'");
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

For the database account, create a dedicated user and grant only the statements the application runs:

CREATE USER 'shop_app'@'localhost' IDENTIFIED BY 'a-long-random-secret';
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.* TO 'shop_app'@'localhost';
RiskDefence in PHP
SQL injectionPDO prepared statements, allow-lists for identifiers
XSShtmlspecialchars with ENT_QUOTES and UTF-8, Content-Security-Policy
CSRFrandom_bytes token, hash_equals check, SameSite cookies
Password leakspassword_hash and password_verify
Malicious uploadsfinfo type check, random names, storage outside web root
Information leaksdisplay_errors off, logs kept private
Vulnerable librariescomposer audit and regular updates

Access control deserves the same care. Every request that reads or changes a record must check that the logged-in user is allowed to touch that specific record, not just that someone is logged in. Changing /orders/101 to /orders/102 in the address bar should never show another customer's order.

Practical exercise: audit and fix a guestbook

On your own machine, write a deliberately weak guestbook: a form posting a name and message, saved with a string-joined SQL query and printed without escaping. Then fix it.

  • Show that a message containing <b> tags renders as bold, proving output is not escaped.
  • Convert the insert and select queries to prepared statements.
  • Escape every printed value with your e() helper.
  • Add a CSRF token and confirm a form submitted without it receives a 403 response.
  • Create a limited database user and switch the app to it.

You are done when HTML in messages appears as plain text, a quote mark in a name no longer breaks the query, and forged form posts are rejected. These habits are practised throughout the advanced PHP programme.

Common mistakes

  • Escaping input with addslashes() or htmlspecialchars() before saving it, instead of using prepared statements and escaping on output.
  • Relying on JavaScript or HTML form validation, which any visitor can bypass.
  • Comparing tokens with == instead of hash_equals().
  • Leaving display_errors on in production, which reveals file paths and query details.
  • Connecting to the database as root, so a single injection bug exposes every database on the server.
  • Hashing passwords with md5() or sha1(), which are too fast to resist guessing.

Frequently asked questions

Do frameworks make PHP secure automatically?

Frameworks such as Laravel and CodeIgniter provide query builders, escaping helpers and CSRF protection, but you can still bypass them with raw queries or unescaped output. You need to understand the threats to use the tools correctly.

Is mysqli safe, or must I use PDO?

Both support prepared statements and both are safe when used correctly. PDO works with many databases through one interface and supports named placeholders, which is why many projects prefer it.

Should I sanitise all input on arrival?

Validate it on arrival and reject what is not acceptable, but do not try to clean it for every possible output. Store the original value and escape it for each context, such as HTML, URL or SQL, at the point of use.

Does WordPress follow the same rules?

Yes. WordPress offers its own functions, such as esc_html() and $wpdb->prepare(), built on the same ideas. The WordPress guide explains how themes and plugins are structured.

Want to Learn This Properly?

The Advanced PHP Web Development & Full-Stack Application Development (Online) turns guides like this into practised skills, with projects on real websites.

WhatsApp Call Us Now

Free counselling

Send an Enquiry

Tell us which course you are interested in. We reply by email or phone with the next batch and how to enrol.

We reply by email or phone within working hours. No spam.