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.
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 <script> 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.
Common Errors (and the fix)
- Trusting user input — you used a value straight from $_POST in a query, a filename, or the page. All incoming data is attacker-controlled until you check it. Validate it (is it the right type/range?) and escape it for whatever context it's going into. "It works when I test it" is not the same as "it's safe".
- XSS from un-escaped output — you echoed $_POST["x"] (or a stored value) into HTML without htmlspecialchars() , so a <script> a user submitted now runs for everyone. Escape at the point of output , every time — including inside value="..." attributes (use ENT_QUOTES ).
- "Undefined array key 'name'" — you read $_POST["name"] when the form didn't send it (or hadn't been submitted yet). Read with a fallback: $_POST["name"] ?? "" , and gate processing behind $_SERVER["REQUEST_METHOD"] === "POST" .
- Missing or skipped validation — you relied on the browser's required / type="email" attributes. Those are UX only; a user can disable JS or post directly with curl. Re-check every rule on the server.
- CSRF token always fails / "Invalid token" — usually you forgot session_start() on one of the pages, or compared with == . Call session_start() on both the form and handler pages and compare with hash_equals() .
Pro Tips
- 💡 Validate on input, escape on output. Check correctness when data arrives; escape it for the specific context (HTML, URL, SQL) at the moment you use it — the same value needs different escaping in different places.
- 💡 Always exit; after a redirect. header("Location: /thanks.php"); exit; — without exit the rest of the script keeps running.
- 💡 Detect the real file type. Use finfo on tmp_name instead of trusting the browser-supplied $_FILES[...]["type"] , which is trivial to fake.
📋 Quick Reference — Forms & Input
Tool
Example
What It Does
$ _GET / $ _POST
$ _POST["name"]
Read submitted form data
??
$ _POST["x"] ?? ""
Fallback if the key is missing
trim()
trim(" hi ")
Strip surrounding whitespace
htmlspecialchars()
htmlspecialchars($s, ENT_QUOTES)
Escape HTML to stop XSS
filter_var()
filter_var($e, FILTER_VALIDATE_EMAIL)
Validate email/int/URL/IP
$ _FILES
$ _FILES["avatar"]["tmp_name"]
Access an uploaded file
move_uploaded_file()
move_uploaded_file($tmp, $dest)
Save an upload safely
hash_equals()
hash_equals($a, $b)
Timing-safe token compare (CSRF)
Frequently Asked Questions
Mini-Challenge: A Contact-Form Handler
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.
Lesson Complete!
- ✅ Form data arrives in $_GET (URL), $_POST (body), or $_REQUEST (both) — read with ?? ""
- ✅ Sanitize output with htmlspecialchars() + ENT_QUOTES to stop XSS
- ✅ Validate emails, ints and ranges with filter_var() ; require fields by checking they're not empty after trim()
- ✅ Sticky forms re-display input — always escaped before it re-enters the HTML
- ✅ Uploads come via $_FILES ; check error/size/type, then move_uploaded_file() under a name you generate
- ✅ A per-session CSRF token verified with hash_equals() blocks forged submissions
- ✅ Next lesson: Sessions & Cookies — remember a user across requests, the foundation of logins
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().
Continue this course
- Previous: File Handling
- Next: Sessions & Cookies