PHP Security Checklist for Students – 10 Vulnerabilities to Fix Before You Deploy

CS Student Productivity PHP Security OWASP Top 10 Before You Deploy SQL Injection XSS CSRF Interactive Checklist
CS Student Productivity & Tools — 2026

PHP Security Checklist for Students — 10 Vulnerabilities to Fix Before You Deploy

Almost every student PHP project is vulnerable to SQL injection, XSS, or plain-text password storage. These are not advanced attack techniques — they are beginner mistakes that appear in 90% of student portfolios. This guide shows you every vulnerability, the exact fix, and gives you a scored checklist to verify before making your project public.

🔍 Code scanner demo 💉 SQL injection fix 🔐 XSS + CSRF protection ✅ Scored checklist

When you deploy a PHP project with a live URL — which you should, for your portfolio — real people can access it. Some of those people will probe it for security weaknesses. A URL shortener, a demo ISP system, a hospital management login page — these attract automated scanners within hours of going live.

This is not meant to frighten you. It is meant to give you the 45 minutes of focused work that makes the difference between a vulnerable student project and a project you are genuinely proud to show in interviews. Each fix below takes 5–10 minutes and is one of the most valuable things you can add to your portfolio story.


Quick Vulnerability Scanner — Paste Your PHP Code

Paste a snippet of your PHP code below (like your login function or a database query). The scanner checks for common vulnerability patterns:

🔍 Paste Your PHP Code — Instant Vulnerability Check


The 10 Vulnerabilities — Click Each to See the Fix

1
SQL Injection via String Concatenation
CRITICAL

SQL injection is the most common and dangerous vulnerability in student PHP projects. It occurs when user input is inserted directly into a SQL query using string concatenation. An attacker can type ' OR '1'='1 into a login form and bypass authentication entirely — or worse, dump your entire database.

// VULNERABLE — never do this
$id = $_GET['id'];  // Could be: 1 OR 1=1
$sql = "SELECT * FROM customers WHERE id = '$id'";
mysqli_query($conn, $sql);

// Login bypass: enter username: admin'-- and any password
$sql = "SELECT * FROM users WHERE username='$u' AND password='$p'";
// With u = "admin'--", the password check is commented out!
// FIXED — always use prepared statements
$id = (int)$_GET['id']; // Cast to int for numeric IDs — fastest fix

// For strings: use prepared statement with ? placeholder
$stmt = mysqli_prepare($conn,
    "SELECT * FROM customers WHERE id = ?");
mysqli_stmt_bind_param($stmt, 'i', $id); // 'i'=int, 's'=string
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
2
Plain Text Password Storage
CRITICAL

Storing passwords as plain text means that anyone who accesses your database — through a SQL injection attack, a server breach, or even a careless phpMyAdmin session left open — can read every user’s password instantly. MD5 and SHA1 are also insecure — they are too fast to compute and have been cracked extensively. PHP’s password_hash() uses bcrypt by default, which is deliberately slow and virtually impossible to crack at scale.

// VULNERABLE — all three of these are wrong
$password = $_POST['password'];           // Plain text — worst
$password = md5($_POST['password']);     // MD5 — broken, do NOT use
$password = sha1($_POST['password']);    // SHA1 — also broken
$sql = "SELECT * FROM users WHERE password='$password'";
// FIXED — use password_hash() to store, password_verify() to check
// When creating/saving a password:
$hash = password_hash($_POST['password'], PASSWORD_DEFAULT);
// Store $hash in database — it looks like: $2y$10$abc123...

// When verifying login:
$stmt = mysqli_prepare($conn,
    "SELECT password_hash FROM users WHERE username = ?");
mysqli_stmt_bind_param($stmt, 's', $username);
mysqli_stmt_execute($stmt);
$user = mysqli_fetch_assoc(mysqli_stmt_get_result($stmt));
if ($user && password_verify($_POST['password'], $user['password_hash'])) {
    // Login successful
}
3
XSS — Cross-Site Scripting via Unescaped Output
CRITICAL

XSS occurs when user-submitted content is displayed on a page without escaping. If a user enters <script>alert('hacked')</script> into a name field and you display it directly, the script executes in every visitor’s browser. In real attacks, XSS is used to steal session cookies (and thus hijack logged-in accounts) or redirect users to malicious sites.

// VULNERABLE — direct echo of user data
echo "Welcome, " . $_SESSION['username'];
echo "<td>" . $row['customer_name'] . "</td>";
echo "Search results for: " . $_GET['q']; // Reflected XSS
// FIXED — wrap every output of user-supplied data in htmlspecialchars()
echo "Welcome, " . htmlspecialchars($_SESSION['username'], ENT_QUOTES, 'UTF-8');
echo "<td>" . htmlspecialchars($row['customer_name'], ENT_QUOTES, 'UTF-8') . "</td>";

// Shorthand helper function — define once, use everywhere:
function h($str) {
    return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
echo "Welcome, " . h($_SESSION['username']); // Clean!
4
No CSRF Protection on State-Changing Forms
HIGH

CSRF (Cross-Site Request Forgery) tricks a logged-in user into unknowingly submitting a form on your site. An attacker embeds a hidden form on their own website that POSTs to your delete or update endpoint. If the victim visits their page while logged into yours, the action executes as that user. CSRF tokens prevent this by requiring a secret value that only your site knows.

// VULNERABLE — no CSRF token — anyone can submit this form remotely
if ($_POST['delete_customer']) {
    $id = (int)$_POST['id'];
    mysqli_query($conn, "DELETE FROM customers WHERE id='$id'");
}
// FIXED — generate token in session, embed in form, verify on submission
if (session_status() === PHP_SESSION_NONE) session_start();

// Generate token once per session (in your auth_check.php or similar):
if (!isset($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

// In your HTML form — add this hidden input:
// <input type="hidden" name="csrf_token"
//        value="<?= htmlspecialchars($_SESSION['csrf_token']) ?>">

// On form submission — verify before doing anything:
if (!isset($_POST['csrf_token'])
    || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) {
    die('CSRF token mismatch — request rejected.');
}
// Safe to proceed with the action now
5
Session Fixation — No session_regenerate_id() After Login
HIGH

If a session ID is not regenerated immediately after login, an attacker who obtained the pre-login session ID (perhaps via XSS on the login page) can use it to hijack the authenticated session. This is called session fixation. The fix is one line added immediately after successful authentication.

// VULNERABLE — session ID is the same before and after login
if ($user && password_verify($password, $user['hash'])) {
    $_SESSION['user_id'] = $user['id'];  // Old session ID still valid!
    header('Location: dashboard.php');
}
// FIXED — one line immediately after successful login
if ($user && password_verify($password, $user['hash'])) {
    session_regenerate_id(true); // ← THIS LINE — new ID, old one invalid
    $_SESSION['user_id'] = $user['id'];
    header('Location: dashboard.php');
}
6
Unrestricted File Upload — No Type or Size Validation
HIGH

A file upload form without validation allows an attacker to upload a PHP file disguised as a profile photo. If that PHP file lands in a web-accessible directory, the attacker can execute arbitrary code on your server by visiting the uploaded file’s URL. This is one of the most devastating web vulnerabilities.

// VULNERABLE — no type check, uses original filename (attacker controls)
move_uploaded_file($_FILES['photo']['tmp_name'],
    'uploads/' . $_FILES['photo']['name']); // Can be "shell.php"!
// FIXED — validate type via MIME and extension, rename to safe filename
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$allowed_exts  = ['jpg', 'jpeg', 'png', 'gif'];
$max_size      = 2 * 1024 * 1024; // 2MB

$file    = $_FILES['photo'];
$mime    = mime_content_type($file['tmp_name']); // Check actual file content
$ext     = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

if (!in_array($mime, $allowed_types)
    || !in_array($ext, $allowed_exts)
    || $file['size'] > $max_size) {
    die('Invalid file type or size.');
}
// Rename to prevent path traversal and PHP execution
$safe_name = uniqid('img_', true) . '.' . $ext;
move_uploaded_file($file['tmp_name'], 'uploads/' . $safe_name);
7
Credentials Committed to GitHub — config in public repo
MEDIUM

Database passwords, Gmail app passwords, and API keys committed to a public GitHub repository are exposed to automated bots that scan GitHub continuously. These bots find credentials within minutes and use them to access your database or send spam emails from your account. The fix is to add config files to .gitignore before the first commit — fixing it after the commit requires deleting the entire git history, which is complex.

.gitignore — add to your project root to prevent credential commits

# Add this to your .gitignore file
config/dbconnection.php
config/email_config.php
*.env
.env
config.php
# Template versions (no real credentials) are fine to commit:
# config/dbconnection.example.php
8
PHP Errors Displayed on Live Server
MEDIUM

When PHP errors are displayed on screen in production, they reveal your file structure, database names, and code logic to visitors. An attacker can deliberately trigger errors to gather information about your system. Add this to the top of your main config file on the live server:

// In your config or bootstrap file — production settings
ini_set('display_errors', '0');      // Hide from browser
ini_set('log_errors', '1');          // Log to server file instead
error_reporting(E_ALL);              // Still record all errors
// On XAMPP (development): set display_errors to '1' for debugging
9
No Input Validation — Accepting Any Data from Forms
MEDIUM

Even with prepared statements, accepting any value in a form field causes problems. An age field that accepts -9999 or a status field that accepts '; DROP TABLE can cause data corruption or unexpected application behaviour. Validate that inputs match the expected type and range before processing.

// Validate expected data types before using them
$age    = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT,
              ['options' => ['min_range' => 1, 'max_range' => 120]]);
$email  = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
$status = in_array($_POST['status'], ['active', 'inactive', 'suspended'])
          ? $_POST['status'] : null; // Whitelist allowed values

if (!$age || !$email || !$status) {
    $errors[] = 'Please provide valid input for all fields.';
}
10
Missing Security Headers
MEDIUM

HTTP security headers instruct the browser to apply extra protections. They are set in PHP with header() and provide a significant security improvement for minimal effort. Add them to your auth_check.php or a global header file so they apply to every page.

// Add to your auth_check.php or main config — applies to every page
header('X-Content-Type-Options: nosniff');     // Prevent MIME sniffing
header('X-Frame-Options: DENY');                // Block clickjacking iframes
header('X-XSS-Protection: 1; mode=block');      // Browser XSS filter
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'; "
     . "script-src 'self' cdn.jsdelivr.net; "  // Allow Bootstrap CDN
     . "style-src 'self' 'unsafe-inline' cdn.jsdelivr.net");

Your Security Checklist — Score Your Project

🛡️ Pre-Deployment Security Checklist 0 / 20 fixed

Start ticking — each item you fix makes your project safer
Critical — Fix These Before Going Live
Critical
Critical
Critical
Critical
Critical
High — Fix Before Sharing Publicly
High
High
High
High
High
Medium — Best Practice
Medium
Medium
Medium
Medium
Medium
Medium
Medium
Medium
Medium
Medium

🛡️ Your Project Security Score

Based on your checklist above:

0%
Tick items above to see your security score

Frequently Asked Questions

How do I handle CSRF for AJAX requests (not regular form submissions)?

For AJAX requests, include the CSRF token as a custom HTTP header rather than a form field. Store the token in a meta tag in your HTML head: <meta name="csrf-token" content="<?= $_SESSION['csrf_token'] ?>">. Then in your JavaScript fetch() call, add a header: headers: {'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content}. In your PHP AJAX endpoint, verify with: if ($_SERVER['HTTP_X_CSRF_TOKEN'] !== $_SESSION['csrf_token']) { http_response_code(403); exit; }.

My project already has MD5 passwords stored in the database. How do I migrate to password_hash() without losing users?

You cannot convert existing MD5 hashes to bcrypt hashes directly (that would require the original passwords). The standard migration approach is: Add a new password_hash column to your users table. During login, after the user enters their password: if their row has a password_hash value, verify with password_verify(). If it has an old MD5 password, compare MD5 hashes. If the MD5 check passes, immediately generate a new password_hash() and save it to the new column — this migrates each user the next time they log in. Once all users have migrated (or after a set period), drop the old MD5 column. This “lazy migration” approach is used by production applications.

Is it worth doing all of this for a student project that nobody important will see?

The security work pays off in three ways that directly affect your career. First, your deployed project is indexed by search engines and accessed by automated security scanners within hours — a compromised demo project that sends spam emails or has your database dumped is embarrassing and damages your professional profile. Second, interviewers at technical companies specifically ask about security awareness — the ability to say “I used prepared statements throughout, password_hash() for credentials, and CSRF tokens on state-changing forms” demonstrates professional-level thinking that most candidates lack. Third, the habits you build now carry into your first job — security-conscious code is one of the most valued attributes of early-career developers.


PHP Login System with Remember Me + Session Timeout →

Implements many of these security fixes together

How to Deploy PHP to a Live Server →

Deploy securely after completing this checklist

How to Use GitHub Copilot for PHP →

Use Copilot carefully for security-sensitive code

Download Free PHP Projects →

Apply this security checklist to your downloaded project

Last updated April 2026. Security recommendations based on OWASP Top 10 (2021 edition), PHP.net security documentation, and verified PHP 8.2 security functions. Vulnerability patterns verified against current PHP manual.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top