Forms
By the end of this lesson you'll be able to read form data from $_GET and $_POST , sanitize and validate it, re-display it safely, handle file uploads, and protect your forms with CSRF tokens — the security skills every PHP app depends on.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Where Form Data Arrives: _GET, _POST, _REQUEST
When a visitor submits a form, PHP hands you the data in a superglobal — a built-in array you can read from anywhere. A form with method="get" puts its data in the URL and you read it from $_GET ; a form with method="post" sends it in the hidden request body and you read it from $_POST . Use GET for harmless reads (a search, a page number) where a shareable URL helps, and POST for anything sensitive or state-changing (logins, sign-ups, comments). There's also $_REQUEST , which merges both — convenient, but it hides where the data came from, so prefer the specific one.
2️⃣ Sanitize: Make Input Safe with htmlspecialchars
Never trust user input. If you echo what someone typed straight back onto the page and they typed script , that script runs in your other visitors' browsers — that's an XSS (cross-site scripting) attack. The fix is to escape the output: htmlspecialchars() converts the dangerous characters into harmless HTML entities, so the browser shows the tag as text instead of running it. Pair it with trim() to drop stray whitespace, and the ?? operator to supply a fallback when a field is missing.
See how became <script> in the safe version? Those entities display as literal angle brackets — the browser will never execute them. Pass ENT_QUOTES so both single and double quotes are escaped too, which matters the moment you put a value inside an HTML attribute.
3️⃣ Validate: Check Input Is Correct with filter_var
Sanitizing makes input safe to display ; validating checks it's actually correct — that an email is a real email and an age is a number in range. PHP's filter_var() does this for common types without you writing any regex: FILTER_VALIDATE_EMAIL , FILTER_VALIDATE_INT (with optional min_range / max_range ), FILTER_VALIDATE_URL and more. It returns the cleaned value on success or false on failure. For required fields , the simplest rule is "must not be empty after trim() ". Collect every problem into an $errors array so you can report them all at once.
4️⃣ Re-Display Values Safely (Sticky Forms)
When validation fails you should send the form back with the user's answers still in the boxes — a sticky form — so they don't retype everything. The trap: you're putting user data back into HTML, so the same XSS risk returns. The rule is absolute — escape every value before it re-enters the page , including values you place inside value="..." attributes. A one-line safe() helper that wraps htmlspecialchars() means you can never forget.
The double quote in Bob "the builder" became " , so it can't close the value="..." attribute early and inject new HTML. That single substitution is the difference between a working form and a hijacked one.
5️⃣ File Uploads with _FILES
Uploaded files don't appear in $_POST — they arrive in $_FILES , and only if the form uses method="post" with enctype="multipart/form-data" . PHP saves the upload to a temporary path and gives you its name , type , tmp_name , error and size . Validate all of it: check error === UPLOAD_ERR_OK , cap the size , and whitelist the type. Then move_uploaded_file() moves it out of the temp folder. Crucially, generate your own filename — never reuse the user's, which could be ../../evil.php .
Now you try. The script below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
One more, this time about safe output. A user submitted HTML in a comment — escape it so it can't run.
6️⃣ Protect Forms with a CSRF Token
CSRF (cross-site request forgery) is a sneaky attack: a malicious page makes a logged-in user's browser submit your form without them realising — and because the browser automatically attaches their session cookie, your server thinks it's a genuine request. The defence is a CSRF token : a long random secret you store in the user's session and embed as a hidden field in every state-changing form. When the form comes back, you compare the submitted token to the stored one with hash_equals() (a timing-safe comparison — never use == for secrets). An attacker can't read or guess the token, so forged submissions are rejected.
📋 Quick Reference — Forms & Input
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the read-the-input, validate, escape, respond loop behind every real form.
Practice quiz
Which superglobal holds data from a form submitted with method="post"?
- $_GET
- $_SERVER
- $_POST
- $_COOKIE
Answer: $_POST. A method="post" form sends data in the request body, which PHP exposes in $_POST. method="get" data lands in $_GET.
What does the ?? operator do when reading $_POST["name"] ?? ""?
- Supplies a fallback if the key is missing, avoiding a warning
- Validates the value
- Escapes the value
- Forces it to a string type
Answer: Supplies a fallback if the key is missing, avoiding a warning. The null coalescing operator ?? gives a fallback when the key is absent, so you never hit an 'Undefined array key' warning.
What is the difference between sanitizing and validating input?
- They are the same thing
- Validating escapes HTML; sanitizing checks ranges
- Both only run on the client
- Sanitizing makes input safe to use/display; validating checks it is correct
Answer: Sanitizing makes input safe to use/display; validating checks it is correct. Sanitizing (e.g. htmlspecialchars) makes input safe; validating (e.g. filter_var) confirms it is correct. You usually do both.
Which function escapes < > " ' & so user input can't run as HTML (stopping XSS)?
- strip_tags()
- htmlspecialchars()
- trim()
- filter_var()
Answer: htmlspecialchars(). htmlspecialchars() turns those characters into harmless entities so the browser displays the tag as text instead of running it.
Why pass ENT_QUOTES to htmlspecialchars when putting a value inside value="..."?
- It escapes both single and double quotes so the value can't break out of the attribute
- It speeds up escaping
- It validates the email
- It removes whitespace
Answer: It escapes both single and double quotes so the value can't break out of the attribute. ENT_QUOTES escapes both quote types, so a double quote becomes " and can't close the value="..." attribute early.
What does filter_var($email, FILTER_VALIDATE_EMAIL) return for an invalid email?
- An empty string
- null
- false
- The original string
Answer: false. filter_var returns the cleaned value on success or false on failure, so === false signals an invalid email.
Why isn't client-side (JavaScript) validation enough on its own?
- It is too slow
- Users can disable JS or post directly with curl, bypassing it — the server must re-check
- It can't validate emails
- It only works in Chrome
Answer: Users can disable JS or post directly with curl, bypassing it — the server must re-check. The user controls the client. JS validation is just UX; every rule that matters must be enforced again on the server.
Where do uploaded files arrive in PHP?
- $_POST
- $_GET
- $_SESSION
- $_FILES
Answer: $_FILES. Uploads arrive in $_FILES (with form enctype="multipart/form-data"), giving name, type, tmp_name, error and size.
Which function should compare a submitted CSRF token to the stored one?
- ==
- hash_equals()
- ===
- strcmp()
Answer: hash_equals(). hash_equals() is a timing-safe comparison for secrets. Never use == for token comparison.
For a stored file upload, what name should you save it under?
- The user's original filename
- The browser-supplied type
- A name you generate yourself (e.g. random bytes)
- Always 'upload.tmp'
Answer: A name you generate yourself (e.g. random bytes). Never reuse the user's filename (it could be ../../evil.php). Generate your own name and move it with move_uploaded_file().