PHP Beginner: Forms, Security & Login

GET/POST | Superglobals | Validation | Secure Upload | Sessions

How to run:

1. Save file in htdocs/ex/ (XAMPP) or www/ex/ (Laragon)

2. Open http://localhost/ex/php_forms_security_login_beginner.php

Login: admin / admin123 or student / student123

Section 1: Forms — GET vs POST

Concept

HTML form sends data to server using 2 methods:

GET = data visible in URL, good for search/filter. POST = data hidden, good for login/submit.

Feature GET POST
Data in URL?YesNo
Data size limit~2048 charsNo limit (almost)
Use forSearch, filterLogin, send data
Bookmarkable?YesNo
PHP variable$_GET$_POST

Code Example

<!-- GET Form --> <form method="get" action="page.php"> <input type="text" name="keyword"> <button type="submit">Search</button> </form> <!-- POST Form --> <form method="post" action="page.php"> <input type="text" name="name"> <input type="email" name="email"> <textarea name="message"></textarea> <button type="submit">Send</button> </form> <?php // Read GET data safely $keyword = htmlspecialchars($_GET['keyword'] ?? ''); // Read POST data safely $name = htmlspecialchars($_POST['name'] ?? ''); ?>

GET Form Demo

POST Form Demo

When to use GET vs POST?

Use GET for:

  • Search / Filter
  • Bookmark or share links
  • Non-sensitive data

Use POST for:

  • Login (password)
  • Form with sensitive data
  • File upload
  • Create / Update / Delete

Section 2: Superglobals & Debug Panel

Concept

Superglobals are special PHP variables available everywhere. PHP creates them automatically.

Variable Description Example
$_GETData from URL query string?name=John
$_POSTData from POST formLogin form
$_REQUESTGET+POST+COOKIE combined Avoid!Dangerous - source unclear
$_SERVERServer & request infoMETHOD, User-Agent
$_FILESUploaded filesImages
$_SESSIONSession dataLogin state
$_COOKIECookie dataRemember me
Warning: $_REQUEST mixes GET + POST + COOKIE. An attacker can confuse the data source. Always use $_GET or $_POST directly!

Code — Safe Superglobal Access

<?php // Always use htmlspecialchars() when displaying superglobal values! // Safe GET $keyword = htmlspecialchars($_GET['keyword'] ?? '', ENT_QUOTES, 'UTF-8'); // Safe POST $name = htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES, 'UTF-8'); // Server info echo $_SERVER['REQUEST_METHOD']; // GET or POST echo $_SERVER['SCRIPT_NAME']; // /ex/page.php echo $_SERVER['HTTP_USER_AGENT']; // Browser info // NEVER trust $_REQUEST - use $_GET or $_POST instead! // $bad = $_REQUEST['data']; // <-- AVOID THIS ?>

Debug Panel — Live Superglobal Values

These are real values from the current request, safely escaped with htmlspecialchars().

$_SERVER (selected)
REQUEST_METHODGET
SCRIPT_NAME/php/php_forms_security_login_beginner.php
SERVER_NAMEisophal.com
SERVER_PORT443
HTTP_USER_AGENTMozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
$_SESSION
Array
(
    [csrf_token] => 50474e427c1255ef...
)
$_GET
(empty)
$_COOKIE
(empty)

Section 3: Validation & Sanitization

Concept

Validation = Check if data is correct (format, range, required).

Sanitization = Clean data to remove dangerous content.

Both are needed! Validate first, then sanitize for output.

Function Purpose Example
trim()Remove whitespacetrim(" hello ") = "hello"
filter_var()Validate email, URL, intfilter_var($e, FILTER_VALIDATE_EMAIL)
htmlspecialchars()Escape HTML (prevent XSS)htmlspecialchars($input)

XSS Attack Explained

XSS (Cross-Site Scripting) = attacker injects JavaScript code into your page.

Example: user types <script>alert('hacked')</script> in a form field.

If you echo this without escaping, the browser will run the JavaScript!

BAD (vulnerable):

<?php // DANGEROUS - XSS vulnerability! echo $_POST['name']; // If user types: <script>alert('XSS')</script> // Browser will execute that JavaScript! ?>

GOOD (safe):

<?php // SAFE - escaped output echo htmlspecialchars($_POST['name'] ?? '', ENT_QUOTES, 'UTF-8'); // Browser shows: &lt;script&gt;alert('XSS')&lt;/script&gt; // as plain text, NOT executed! ?>
Demo: Type <script>alert('test')</script> in the validation form below. You will see it displayed as plain text, not executed!

Code — Validation Pattern

<?php $errors = []; $data = []; if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 1) Read & Trim $data['username'] = trim($_POST['username'] ?? ''); $data['email'] = trim($_POST['email'] ?? ''); $data['age'] = trim($_POST['age'] ?? ''); // 2) Validate if ($data['username'] === '') { $errors['username'] = 'Username required!'; } elseif (mb_strlen($data['username']) < 3 || mb_strlen($data['username']) > 20) { $errors['username'] = 'Must be 3-20 characters.'; } if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'Invalid email!'; } $age = filter_var($data['age'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 10, 'max_range' => 80]]); if ($age === false) { $errors['age'] = 'Age must be 10-80.'; } // 3) If valid, process data if (empty($errors)) { echo "All good!"; } } ?> <!-- 4) Show old values + errors --> <input value="<?= htmlspecialchars($data['username'] ?? '') ?>"> <?php if (isset($errors['username'])): ?> <p class="text-red-500"><?= $errors['username'] ?></p> <?php endif; ?>

Validation Lab — Interactive

Fill in the form and submit. Errors will appear per field. Old values are preserved.

Section 4: Secure File Upload

Concept

File upload is powerful but dangerous if done wrong! An attacker could upload a PHP file and execute it on your server.

Security Checklist:

  1. Allow-list extensions — only allow jpg, png, webp (reject php, exe!)
  2. Check MIME type — use finfo_file() or getimagesize()
  3. Max file size — enforce limit (e.g., 2 MB)
  4. Rename file — never trust user filename! Use bin2hex(random_bytes())
  5. Use enctype="multipart/form-data" on the form
  6. Store safely — ideally outside webroot; if not, use random name + safe extension

Code — Secure Upload

<?php $upload_dir = __DIR__ . '/uploads'; if (!is_dir($upload_dir)) mkdir($upload_dir, 0755, true); $allowed_ext = ['jpg','jpeg','png','webp']; $allowed_mime = ['image/jpeg','image/png','image/webp']; $max_size = 2 * 1024 * 1024; // 2 MB if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['photo'])) { $file = $_FILES['photo']; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); // 1) Check extension if (!in_array($ext, $allowed_ext, true)) die('Bad extension!'); // 2) Check size if ($file['size'] > $max_size) die('Too large!'); // 3) Check MIME type $finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($file['tmp_name']); if (!in_array($mime, $allowed_mime, true)) die('Bad MIME!'); // 4) Verify it's a real image if (@getimagesize($file['tmp_name']) === false) die('Not an image!'); // 5) Generate safe filename $safe_name = bin2hex(random_bytes(12)) . '.' . $ext; // 6) Move to uploads/ move_uploaded_file($file['tmp_name'], $upload_dir . '/' . $safe_name); echo "Uploaded as: $safe_name"; } ?> <form method="post" enctype="multipart/form-data"> <input type="file" name="photo" accept="image/*"> <button type="submit">Upload</button> </form>

Upload Demo — Interactive

Allowed: JPG, PNG, WEBP only. Max 2 MB.

Safe File Delete Pattern

<?php // SAFE DELETE - prevent directory traversal! $filename = basename($_POST['del_file'] ?? ''); // basename() removes path $full_path = realpath($upload_dir . '/' . $filename); $real_dir = realpath($upload_dir); // Only delete if file is INSIDE uploads directory if ($full_path && $real_dir && strpos($full_path, $real_dir) === 0) { unlink($full_path); } // If someone tries: ../../config.php -- basename() and realpath() block it! ?>

Section 5: Login with Sessions

Concept

Sessions let you store user data across pages. PHP creates a session ID stored in a cookie.

Session Lifecycle:

  1. session_start() — start/resume session (must be before any HTML output!)
  2. $_SESSION['key'] = value — store data
  3. session_regenerate_id(true) — regenerate ID after login (security!)
  4. session_destroy() — destroy session on logout

Security Rules:

  • Always session_regenerate_id(true) after login to prevent session fixation
  • Store passwords as hashes: password_hash() + password_verify()
  • Never store plain passwords anywhere!
  • Destroy session completely on logout

Code — Session Login Pattern

<?php session_start(); // User database with hashed passwords $users = [ 'admin' => ['pass' => password_hash('admin123', PASSWORD_DEFAULT), 'role' => 'Admin'], ]; // Login if ($_SERVER['REQUEST_METHOD'] === 'POST') { $user = trim($_POST['username'] ?? ''); $pass = $_POST['password'] ?? ''; if (isset($users[$user]) && password_verify($pass, $users[$user]['pass'])) { session_regenerate_id(true); // Prevent session fixation! $_SESSION['logged_in'] = true; $_SESSION['username'] = $user; $_SESSION['role'] = $users[$user]['role']; } } // Check if logged in if (!empty($_SESSION['logged_in'])) { echo "Welcome, " . htmlspecialchars($_SESSION['username']); } // Logout if (($_POST['action'] ?? '') === 'logout') { $_SESSION = []; session_destroy(); header('Location: login.php'); exit; } ?>

Login Demo — Interactive

Test accounts: admin/admin123 or student/student123

How password_hash works

password_hash('admin123', PASSWORD_DEFAULT) generates:

$2y$10$gVFvtns/DiPqiMM.V70BIuwhzCIHmZVVU72nF5.PyfrOqHFT0ddH6

Each call produces a different hash (unique salt). But password_verify() can check any valid hash:

password_verify('admin123', $hash) returns true.

NEVER store plain text passwords!

Practice Questions (22 items)

Part A: GET/POST & Superglobals (6 questions)

Q1 (MCQ): Which method shows data in the URL?

a) POST   b) GET   c) PUT   d) PATCH

Hint: Think about URL parameters like ?key=value

b) GET — GET data appears in the URL as query parameters.

Q2: When should you use POST instead of GET? Give 2 examples.

Hint: Think about sensitive data and data modification.

1) Login forms (passwords are sensitive)
2) Forms that create/update/delete data (e.g., registration, file upload)
POST hides data from URL and has no size limit.

Q3 (MCQ): Which superglobal contains data sent via POST?

a) $_GET   b) $_SERVER   c) $_POST   d) $_FILES

Hint: The name matches the method.

c) $_POST

Q4: What does $_SERVER['REQUEST_METHOD'] return?

Hint: It tells you HOW the page was requested.

It returns the HTTP method used: "GET" or "POST" (as a string).

Q5 (MCQ): Why should you avoid $_REQUEST?

a) It's slow   b) It doesn't work   c) It mixes GET+POST+COOKIE (unclear source)   d) It's deprecated

Hint: Think about security and knowing where data comes from.

c) $_REQUEST combines GET, POST, and COOKIE data. An attacker could manipulate the source. Always use $_GET or $_POST directly.

Q6: Write code to safely read and display a GET parameter called name.

Hint: Use htmlspecialchars() and the null coalescing operator ??

<?php $name = htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8'); echo "Hello, " . $name; ?>

Part B: Validation & Sanitization (8 tasks)

Q7: What is the difference between validation and sanitization?

Hint: One checks, the other cleans.

Validation = checking if data meets requirements (correct format, range).
Sanitization = cleaning data to remove/escape dangerous characters.
Example: Validate email format, sanitize output with htmlspecialchars().

Q8: Write code to validate that an email is valid using filter_var().

Hint: Use FILTER_VALIDATE_EMAIL

<?php $email = trim($_POST['email'] ?? ''); if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { echo "Invalid email format!"; } else { echo "Email is valid: " . htmlspecialchars($email); } ?>

Q9: Write validation for age (must be integer between 10 and 80).

Hint: filter_var with FILTER_VALIDATE_INT and options

<?php $age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT, ['options' => ['min_range' => 10, 'max_range' => 80]]); if ($age === false) { echo "Age must be an integer between 10 and 80."; } ?>

Q10: This code has an XSS vulnerability. Fix it:

<?php echo "Welcome, " . $_GET['name']; ?>

Hint: What happens if name contains <script>?

<?php echo "Welcome, " . htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8'); ?>

Q11: Write code to trim whitespace and validate username is 3-20 characters.

Hint: Use trim() then mb_strlen()

<?php $username = trim($_POST['username'] ?? ''); if ($username === '') { $error = 'Username is required!'; } elseif (mb_strlen($username) < 3 || mb_strlen($username) > 20) { $error = 'Username must be 3-20 characters.'; } ?>

Q12: Write validation for an optional URL field.

Hint: Only validate if not empty. Use FILTER_VALIDATE_URL.

<?php $website = trim($_POST['website'] ?? ''); if ($website !== '' && !filter_var($website, FILTER_VALIDATE_URL)) { $error = 'Invalid URL! Use format: https://example.com'; } ?>

Q13: Write validation for a password field (min 8 chars, must contain a number).

Hint: Use strlen() and preg_match()

<?php $password = $_POST['password'] ?? ''; if (strlen($password) < 8) { $error = 'Password must be at least 8 characters.'; } elseif (!preg_match('/[0-9]/', $password)) { $error = 'Password must contain at least one number.'; } ?>

Q14: What does htmlspecialchars() do? Why is it critical?

Hint: Think about < and > characters.

htmlspecialchars() converts special HTML characters to entities:
< becomes &lt;, > becomes &gt;, " becomes &quot;
This prevents XSS attacks by ensuring user input is displayed as text, not executed as HTML/JS.

Part C: File Upload Security (5 tasks)

Q15: This upload code is insecure. Identify the vulnerability:

<?php $name = $_FILES['file']['name']; move_uploaded_file($_FILES['file']['tmp_name'], "uploads/$name"); echo "Uploaded: $name"; ?>

Hint: What if someone uploads a file called malware.php?

Vulnerabilities:
1) No extension check — user can upload .php files!
2) No MIME type check — file could be disguised
3) No size limit — could fill disk
4) Uses original filename — path traversal risk
5) No XSS protection when echoing filename

Q16: Why should you rename uploaded files instead of using the original name?

Hint: Think about overwriting, special characters, and execution.

1) Prevent name collisions (overwriting existing files)
2) Prevent directory traversal (names like ../../etc/passwd)
3) Prevent execution (names like shell.php)
4) Remove special characters that could cause issues
Best practice: bin2hex(random_bytes(12)) . '.' . $safe_ext

Q17: Write code to check if an uploaded file extension is allowed.

Hint: Use pathinfo() and in_array()

<?php $allowed = ['jpg', 'jpeg', 'png', 'webp']; $ext = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed, true)) { die('File type not allowed! Only: ' . implode(', ', $allowed)); } ?>

Q18: Why is checking only the file extension NOT enough for security?

Hint: Can you rename a .php file to .jpg?

A user can rename any file (e.g., malware.php to malware.jpg). The extension is just text!
You must also check:
1) MIME type using finfo_file() or mime_content_type()
2) For images: getimagesize() returns false if not a real image
Both checks together = much safer.

Q19: Improve this upload code to be secure:

<?php $file = $_FILES['photo']; move_uploaded_file($file['tmp_name'], "uploads/" . $file['name']); echo "Done!"; ?>

Hint: Add extension check, MIME check, size limit, safe name.

<?php $file = $_FILES['photo']; $allowed_ext = ['jpg','jpeg','png','webp']; $allowed_mime = ['image/jpeg','image/png','image/webp']; $max_size = 2 * 1024 * 1024; $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed_ext, true)) die('Bad extension!'); if ($file['size'] > $max_size) die('File too large!'); $mime = (new finfo(FILEINFO_MIME_TYPE))->file($file['tmp_name']); if (!in_array($mime, $allowed_mime, true)) die('Bad MIME type!'); if (@getimagesize($file['tmp_name']) === false) die('Not a real image!'); $safe = bin2hex(random_bytes(12)) . '.' . $ext; move_uploaded_file($file['tmp_name'], "uploads/$safe"); echo "Saved as: " . htmlspecialchars($safe); ?>

Part D: Sessions & Login (3 tasks)

Q20: This login code has a security issue. Fix it:

<?php session_start(); if ($_POST['user'] === 'admin' && $_POST['pass'] === 'admin123') { $_SESSION['logged_in'] = true; $_SESSION['username'] = $_POST['user']; } ?>

Hint: What about session fixation? And plain text password?

<?php session_start(); $users = ['admin' => password_hash('admin123', PASSWORD_DEFAULT)]; $user = trim($_POST['user'] ?? ''); $pass = $_POST['pass'] ?? ''; if (isset($users[$user]) && password_verify($pass, $users[$user])) { session_regenerate_id(true); // Prevent session fixation! $_SESSION['logged_in'] = true; $_SESSION['username'] = $user; } ?>
Fixes: 1) Use password_hash/verify instead of plain text. 2) Add session_regenerate_id(true).

Q21: Write code to protect a page so only logged-in users can access it.

Hint: Check $_SESSION and redirect if not logged in.

<?php session_start(); if (empty($_SESSION['logged_in'])) { header('Location: login.php'); exit; // IMPORTANT: always exit after redirect! } // Protected content below echo "Welcome, " . htmlspecialchars($_SESSION['username']); ?>

Q22: Write a complete, safe logout script.

Hint: Clear session data, destroy session, delete cookie.

<?php session_start(); // 1) Clear all session data $_SESSION = []; // 2) Delete session cookie if (ini_get('session.use_cookies')) { $p = session_get_cookie_params(); setcookie(session_name(), '', time() - 42000, $p['path'], $p['domain'], $p['secure'], $p['httponly']); } // 3) Destroy session session_destroy(); // 4) Redirect to login page header('Location: login.php'); exit; ?>

Mini Challenge

Challenge 1: Build a Secure Contact Form

Build a single-file PHP page with:

  • A POST form with fields: name, email, subject, message
  • trim() all inputs
  • Validate: all required, email must be valid, message min 10 chars
  • Show per-field errors using red text
  • Keep old input values after submit
  • Use htmlspecialchars() for ALL output
  • Add a CSRF token
  • If valid, show a success message with the submitted data (safely escaped)
<?php session_start(); if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(32)); $errors = []; $data = ['name'=>'','email'=>'','subject'=>'','message'=>'']; $success = false; if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) die('CSRF error'); $data['name'] = trim($_POST['name'] ?? ''); $data['email'] = trim($_POST['email'] ?? ''); $data['subject'] = trim($_POST['subject'] ?? ''); $data['message'] = trim($_POST['message'] ?? ''); if ($data['name'] === '') $errors['name'] = 'Required!'; if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) $errors['email'] = 'Invalid email!'; if ($data['subject'] === '') $errors['subject'] = 'Required!'; if (mb_strlen($data['message']) < 10) $errors['message'] = 'Min 10 characters!'; if (empty($errors)) $success = true; } function e($s) { return htmlspecialchars($s ?? '', ENT_QUOTES, 'UTF-8'); } ?> <form method="post"> <input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>"> <label>Name:</label> <input name="name" value="<?= e($data['name']) ?>"> <?php if (isset($errors['name'])): ?> <p style="color:red"><?= $errors['name'] ?></p> <?php endif; ?> <!-- Repeat for email, subject, message... --> <button type="submit">Send</button> </form> <?php if ($success): ?> <h3>Success!</h3> <p>Name: <?= e($data['name']) ?></p> <p>Email: <?= e($data['email']) ?></p> <p>Subject: <?= e($data['subject']) ?></p> <p>Message: <?= e($data['message']) ?></p> <?php endif; ?>

Challenge 2: Protect a Page with Session Login

Create a page that:

  • Shows a login form if user is NOT logged in
  • Uses password_hash/password_verify (never plain text!)
  • On successful login: session_regenerate_id(true)
  • Shows protected content only when logged in
  • Has a working logout button that fully destroys the session
<?php session_start(); $users = ['admin' => password_hash('secret', PASSWORD_DEFAULT)]; // Logout if (($_POST['action'] ?? '') === 'logout') { $_SESSION = []; session_destroy(); header('Location: ' . $_SERVER['PHP_SELF']); exit; } // Login if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['user'])) { $u = trim($_POST['user'] ?? ''); $p = $_POST['pass'] ?? ''; if (isset($users[$u]) && password_verify($p, $users[$u])) { session_regenerate_id(true); $_SESSION['logged_in'] = true; $_SESSION['username'] = $u; } else { $error = 'Invalid credentials.'; } } ?> <?php if (empty($_SESSION['logged_in'])): ?> <form method="post"> <input name="user" placeholder="Username"> <input name="pass" type="password" placeholder="Password"> <button>Login</button> </form> <?php if (!empty($error)) echo "<p style='color:red'>$error</p>"; ?> <?php else: ?> <h2>Welcome, <?= htmlspecialchars($_SESSION['username']) ?>!</h2> <p>This is protected content.</p> <form method="post"> <input type="hidden" name="action" value="logout"> <button>Logout</button> </form> <?php endif; ?>

Answer Key

Click to reveal all answers at once.

# Quick Answer
Q1b) GET
Q2Login (sensitive data), Create/Update/Delete operations
Q3c) $_POST
Q4Returns "GET" or "POST" (the HTTP method string)
Q5c) It mixes GET+POST+COOKIE (unclear data source)
Q6htmlspecialchars($_GET['name'] ?? '', ENT_QUOTES, 'UTF-8')
Q7Validation = check format; Sanitization = clean/escape data
Q8filter_var($email, FILTER_VALIDATE_EMAIL)
Q9filter_var($age, FILTER_VALIDATE_INT, [options => [min_range=>10, max_range=>80]])
Q10Add htmlspecialchars() around $_GET['name']
Q11trim() then check mb_strlen() between 3-20
Q12Only validate if not empty; use FILTER_VALIDATE_URL
Q13strlen() >= 8 and preg_match('/[0-9]/', $pass)
Q14Converts < > " & to HTML entities to prevent XSS
Q15No ext check, no MIME check, no size limit, uses original filename
Q16Prevent collisions, traversal, execution; use random name
Q17pathinfo() for ext + in_array() against allow-list
Q18Extensions can be faked; also need finfo MIME check + getimagesize()
Q19Add ext+MIME+size checks, random filename, getimagesize()
Q20Use password_hash/verify + session_regenerate_id(true)
Q21Check $_SESSION['logged_in'], redirect + exit if not set
Q22$_SESSION=[]; delete cookie; session_destroy(); redirect+exit