PHP Security – Practical Tutorial
100%

PHP Security

PHP security means protecting your application, database, and users from attacks and unauthorized access. Common risks include SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), insecure file uploads, and weak authentication.

In this lesson, you will learn practical techniques for securing PHP applications such as student management systems, library management systems, and online admission portals.

1. Validate and Sanitize User Input

Never trust user input. Validate input on the server before using it. Sanitization can transform input, but it does not replace validation or context-specific output escaping.

<?php
$name = trim($_POST["name"] ?? "");
$email = trim($_POST["email"] ?? "");

if ($name === "") {
    exit("Name is required.");
}

if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
    exit("Please enter a valid email address.");
}

echo "Input validation successful.";
?>

Important points:

  • Validate required fields.
  • Check email addresses with FILTER_VALIDATE_EMAIL.
  • Validate numeric values and allowed ranges.
  • Apply server-side validation even when JavaScript validation exists.
  • Escape values when displaying them in HTML.

Validation checks whether data meets your requirements. Escaping protects the context where data is displayed.

2. Prevent SQL Injection

SQL injection happens when untrusted input changes the meaning of a database query. Use PDO prepared statements instead of joining user input directly into SQL.

Unsafe approach:

<?php
// Never build SQL this way with user input.
$sql = "SELECT * FROM students WHERE email = '$email'";
?>

Safe approach using PDO:

<?php
$email = $_POST["email"] ?? "";

$stmt = $pdo->prepare(
    "SELECT id, name, email
     FROM students
     WHERE email = :email"
);

$stmt->execute([
    "email" => $email
]);

$student = $stmt->fetch(PDO::FETCH_ASSOC);

if ($student) {
    echo "Student found.";
} else {
    echo "No student found.";
}
?>

This example assumes $pdo is an existing PDO connection.

Prepared statements separate SQL instructions from parameter values. They are one of the most important defenses against SQL injection.

Placeholders cannot be used for table names or column names. If identifiers must be dynamic, select them from a fixed allowlist.

3. Prevent Cross-Site Scripting (XSS)

Cross-site scripting (XSS) can occur when untrusted data is inserted into a webpage as executable content. Escape text before displaying it in HTML.

<?php
$name = $_GET["name"] ?? "";

$safeName = htmlspecialchars(
    $name,
    ENT_QUOTES | ENT_SUBSTITUTE,
    "UTF-8"
);

echo "<h2>Welcome, " . $safeName . "</h2>";
?>

If someone enters HTML or JavaScript as the name, the escaped value is displayed as text rather than being interpreted as HTML markup.

Remember:

  • Escape data according to its output context.
  • Use htmlspecialchars() for ordinary HTML text and quoted attributes.
  • Do not insert untrusted values directly into JavaScript or CSS.
  • Validate input separately; escaping output is not input validation.
4. Secure Password Storage

Never store user passwords as plain text. PHP provides password_hash() to create password hashes and password_verify() to check passwords.

Creating a password hash:

<?php
$password = $_POST["password"] ?? "";

if ($password === "") {
    exit("Password is required.");
}

$hash = password_hash(
    $password,
    PASSWORD_DEFAULT
);

// Store $hash in the database.
// Never store the original password.
?>

Verifying a password during login:

<?php
$password = $_POST["password"] ?? "";
$storedHash = $user["password_hash"];

if (password_verify($password, $storedHash)) {
    echo "Password is correct.";
} else {
    echo "Invalid login credentials.";
}
?>

The second example assumes that $user["password_hash"] was retrieved from a trusted database record.

Use a database column large enough for password hashes, such as VARCHAR(255). Never log passwords, and use rate limiting to reduce repeated login attempts.

5. Secure PHP Sessions

Sessions allow PHP applications to maintain user state across requests. Configure session cookies securely and regenerate the session ID after authentication.

<?php
session_set_cookie_params([
    "lifetime" => 0,
    "path" => "/",
    "secure" => true,
    "httponly" => true,
    "samesite" => "Lax"
]);

session_start();

// After successful password verification:
session_regenerate_id(true);

$_SESSION["user_id"] = $userId;
?>

Call the session configuration and session_start() before sending output. The example assumes a successful login and a valid $userId.

  • secure restricts the cookie to HTTPS connections.
  • httponly prevents JavaScript from reading the cookie.
  • samesite helps reduce certain cross-site request risks.
  • session_regenerate_id(true) helps prevent session fixation after login.

Use HTTPS in production. The secure option should be enabled for HTTPS sites; a local HTTP-only development environment needs a suitable configuration.

6. Prevent Cross-Site Request Forgery (CSRF)

CSRF attacks attempt to make an authenticated user's browser submit an unwanted request. Use CSRF tokens for state-changing form submissions.

Generate a token:

<?php
session_start();

if (empty($_SESSION["csrf_token"])) {
    $_SESSION["csrf_token"] = bin2hex(
        random_bytes(32)
    );
}

$csrfToken = $_SESSION["csrf_token"];
?>

<form method="post" action="update_profile.php">
    <input
        type="hidden"
        name="csrf_token"
        value="<?= htmlspecialchars(
            $csrfToken,
            ENT_QUOTES,
            'UTF-8'
        ) ?>"
    >

    <button type="submit">Update Profile</button>
</form>

Validate the token in update_profile.php:

<?php
session_start();

$submittedToken = $_POST["csrf_token"] ?? "";
$sessionToken = $_SESSION["csrf_token"] ?? "";

if (
    !is_string($submittedToken) ||
    !is_string($sessionToken) ||
    $sessionToken === "" ||
    !hash_equals($sessionToken, $submittedToken)
) {
    http_response_code(403);
    exit("Invalid request.");
}

// Validate the user's permissions and form data here.
// Perform the requested update only after validation.
?>

Keep the token in the session, validate it on the server, and use it for every relevant state-changing request. CSRF tokens do not replace authentication or authorization.

7. Secure File Uploads

File uploads can expose an application to malicious files, code execution, and storage abuse. Validate uploads on the server and use generated filenames.

<?php
if (
    !isset($_FILES["photo"]) ||
    !is_array($_FILES["photo"])
) {
    exit("No file uploaded.");
}

$file = $_FILES["photo"];

if ($file["error"] !== UPLOAD_ERR_OK) {
    exit("Upload failed.");
}

$maxSize = 2 * 1024 * 1024;

if ($file["size"] > $maxSize) {
    exit("File is too large.");
}

$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file["tmp_name"]);

$allowedTypes = [
    "image/jpeg" => "jpg",
    "image/png" => "png"
];

if (!isset($allowedTypes[$mime])) {
    exit("Only JPEG and PNG images are allowed.");
}

$uploadDir = __DIR__ . "/private_uploads";

if (
    !is_dir($uploadDir) &&
    !mkdir($uploadDir, 0750, true) &&
    !is_dir($uploadDir)
) {
    exit("Upload directory unavailable.");
}

$filename = bin2hex(random_bytes(16))
    . "." . $allowedTypes[$mime];

$destination = $uploadDir . "/" . $filename;

if (!move_uploaded_file(
    $file["tmp_name"],
    $destination
)) {
    exit("Unable to save uploaded file.");
}

echo "File uploaded successfully.";
?>

The upload form must use enctype="multipart/form-data".

Store uploaded files outside the public web root when possible, restrict access, and configure the server so uploaded content cannot execute as code.

For production use, also validate dimensions when appropriate, set upload limits in PHP configuration, and handle directory permissions and cleanup securely.

8. Hide Errors and Protect Configuration Files

Detailed error messages can reveal file paths, database details, and other information that attackers could use. Display general messages to visitors and log technical details privately.

Production configuration example:

<?php
ini_set("display_errors", "0");
ini_set("log_errors", "1");

error_reporting(E_ALL);

// Handle errors using your application's logging system.
?>

These settings should be configured appropriately for your hosting environment. Some PHP settings are controlled by the server configuration.

Protect application configuration:

  • Do not commit database passwords or API secrets to a public repository.
  • Keep sensitive configuration files outside the public web root where practical.
  • Use environment variables or a secure configuration mechanism.
  • Restrict database permissions to only what the application requires.
  • Keep PHP and third-party packages updated.
  • Use HTTPS and restrict access to logs and backups.
9. Practical Example: Secure Student Login

This example demonstrates prepared statements, password verification, session regeneration, and a generic login failure message.

It assumes a PDO connection named $pdo and a database table containing id, email, and password_hash columns.

<?php
session_set_cookie_params([
    "lifetime" => 0,
    "path" => "/",
    "secure" => true,
    "httponly" => true,
    "samesite" => "Lax"
]);

session_start();

$error = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $email = $_POST["email"] ?? "";
    $password = $_POST["password"] ?? "";

    if (
        !is_string($email) ||
        !is_string($password) ||
        !filter_var($email, FILTER_VALIDATE_EMAIL)
    ) {
        $error = "Invalid email or password.";
    } else {
        try {
            $stmt = $pdo->prepare(
                "SELECT id, password_hash
                 FROM students
                 WHERE email = :email
                 LIMIT 1"
            );

            $stmt->execute([
                "email" => $email
            ]);

            $user = $stmt->fetch(PDO::FETCH_ASSOC);

            if (
                $user &&
                password_verify(
                    $password,
                    $user["password_hash"]
                )
            ) {
                session_regenerate_id(true);

                $_SESSION["student_id"] = (int) $user["id"];

                header("Location: student_dashboard.php");
                exit;
            }

            $error = "Invalid email or password.";

        } catch (PDOException $e) {
            error_log($e->getMessage());

            $error = "Login is temporarily unavailable.";
        }
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Student Login</title>
</head>
<body>

    <h2>Student Login</h2>

    <?php if ($error !== ""): ?>
        <p style="color: red;">
            <?= htmlspecialchars(
                $error,
                ENT_QUOTES | ENT_SUBSTITUTE,
                "UTF-8"
            ) ?>
        </p>
    <?php endif; ?>

    <form method="post">
        <label for="email">Email:</label>
        <input
            type="email"
            id="email"
            name="email"
            required
        >

        <br><br>

        <label for="password">Password:</label>
        <input
            type="password"
            id="password"
            name="password"
            required
        >

        <br><br>

        <button type="submit">Login</button>
    </form>

</body>
</html>

Security requirements for a real application:

  • Use HTTPS and secure session configuration.
  • Add rate limiting and appropriate login monitoring.
  • Use CSRF protection for relevant state-changing forms.
  • Check authorization on every protected page.
  • Ensure the database stores password hashes, not plain-text passwords.
  • Regenerate the session ID after successful authentication.
  • Use generic login errors to reduce account enumeration.

The example illustrates core techniques, but it is not a complete production authentication system.


🧠 Quick Quiz

1. Which PHP function helps prevent SQL injection when used with PDO?




2. Which function verifies a password against a stored password hash?




3. What is the purpose of a CSRF token?