Security

By the end of this lesson you'll be able to spot and fix the most common PHP security holes — SQL injection, XSS, and CSRF — and harden passwords, sessions, file uploads, and HTTP headers the way real production apps do.

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️⃣ SQL Injection — Prepared Statements

SQL injection is the classic, still-number-one web attack. It happens when you glue user input directly into a SQL string: the input then stops being data and becomes part of the command . The fix is a prepared statement — you send the query with a ? placeholder first, then send the value separately, so the database can never confuse the two. This is exactly why the Database lesson taught you prepare() and execute() .

Read the vulnerable query out loud: the attacker's ' OR '1'='1 closes your quote and adds a condition that's always true . With a prepared statement the value stays in its own lane — there's no way to "escape" into the SQL.

2️⃣ XSS — Escape Output & Use a CSP

XSS (cross-site scripting) is the mirror image of SQL injection, but the victim is the browser instead of the database. If you echo untrusted text into a page without escaping it, an attacker can slip in a script tag that then runs in every visitor's browser — stealing cookies or hijacking the session. The fix: run every piece of untrusted output through htmlspecialchars() , which converts " ' & into harmless entities. Add a Content-Security-Policy header as a second wall.

In the safe output the became < , so the browser displays the text instead of running it. Always escape at the moment of output, and always say which charset ( 'UTF-8' ) you mean.

3️⃣ CSRF — Per-Session Tokens

CSRF (cross-site request forgery) tricks a logged-in user's browser into firing a request at your site — say, "change my email" — without them realising. Because the browser automatically attaches their session cookie, your server thinks it's genuine. The defence is a secret token that only your own pages know: put a random value in a hidden form field, store the same value in the session, and reject any POST whose token doesn't match. Compare with hash_equals() , not === , so the comparison takes constant time and leaks no timing clues.

4️⃣ Passwords, Sessions & Cookies

Never store a real password — store a hash . Use password_hash() (bcrypt/Argon2): it's deliberately slow, salts every hash for you, and pairs with password_verify() at login. Then lock down the session cookie : httponly hides it from JavaScript (blunting XSS theft), secure sends it only over HTTPS, and samesite=Strict keeps it off cross-site requests (blunting CSRF). Call session_regenerate_id() right after login to defeat session fixation.

5️⃣ Validate Input, Uploads, SSRF & Headers

The thread running through every section is one rule: never trust user input . Validate its shape with filter_var() . For file uploads , trust the bytes (the real MIME type) not the filename, give the file a random name, and store it outside the webroot so it can't be executed. For SSRF , never fetch a user-supplied URL without an allow-list of permitted hosts. And set security headers — X-Content-Type-Options , X-Frame-Options , Strict-Transport-Security , Content-Security-Policy — on every response.

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. Turn a dangerous concatenated query into a parameterised one by placing a single ? where the value belongs.

📋 Quick Reference — Threat → Defence

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 combines CSRF checking and XSS escaping — the two defences you'll wire into almost every form you ever build.

Practice quiz

What is the correct defence against SQL injection?

  • Escaping quotes with addslashes()
  • Removing spaces from input
  • Prepared statements with bound parameters
  • Using md5() on the query

Answer: Prepared statements with bound parameters. A prepared statement sends the SQL and the value on separate channels, so user input can never become part of the command.

Why is "' OR '1'='1" dangerous when concatenated into a query?

  • It closes the quote and adds an always-true condition, matching every row
  • It crashes the database
  • It deletes the users table
  • It is harmless text

Answer: It closes the quote and adds an always-true condition, matching every row. The injected condition is always true, so WHERE matches every row and the attacker logs in as the first user.

Which function escapes output to prevent XSS?

  • strip_tags() only
  • urlencode()
  • json_encode()
  • htmlspecialchars($x, ENT_QUOTES, 'UTF-8')

Answer: htmlspecialchars($x, ENT_QUOTES, 'UTF-8'). htmlspecialchars() converts < > " ' & into HTML entities so the browser displays untrusted text instead of executing it.

When should you escape data for XSS protection?

  • Before storing it in the database
  • At the moment you output it into HTML
  • Only once, at registration
  • Never, the database handles it

Answer: At the moment you output it into HTML. Store the raw value and escape only at output; escaping before storage leads to double-escaped, mangled text.

How should a CSRF token be compared to avoid timing attacks?

  • With hash_equals(), which compares in constant time
  • With === for speed
  • With == loose comparison
  • With strcmp()

Answer: With hash_equals(), which compares in constant time. hash_equals() compares in constant time so it leaks no timing information about the secret token.

Why must you never use md5() or sha1() for passwords?

  • They are deprecated syntax
  • They only work on numbers
  • They are fast checksums a GPU can crack billions/sec, and aren't salted
  • They produce hashes that are too long

Answer: They are fast checksums a GPU can crack billions/sec, and aren't salted. Password hashing must be slow and salted; password_hash() (bcrypt/Argon2) is built for this, md5/sha1 are not.

Which function verifies a password against a stored hash?

  • password_check()
  • password_verify($input, $hash)
  • hash_compare()
  • md5($input) === $hash

Answer: password_verify($input, $hash). password_verify() safely checks a plaintext password against a hash created by password_hash().

What do the httponly, secure, and samesite cookie flags do?

  • Compress the cookie
  • Encrypt the session data
  • Make the cookie never expire
  • Hide it from JavaScript, send only over HTTPS, and limit cross-site requests

Answer: Hide it from JavaScript, send only over HTTPS, and limit cross-site requests. HttpOnly blunts XSS theft, Secure forces HTTPS, and SameSite limits CSRF; set them before session_start().

Which function call defeats session fixation after login?

  • session_destroy()
  • session_regenerate_id(true)
  • session_unset()
  • setcookie('PHPSESSID', '')

Answer: session_regenerate_id(true). session_regenerate_id(true) issues a brand-new id and deletes the old one, so any planted id becomes worthless.

How should you handle a file upload safely?

  • Trust the file extension and keep the original name
  • Store it in the same folder as your scripts
  • Check the real MIME type, rename it randomly, and store it outside the webroot
  • Only check that the size is under 10 MB

Answer: Check the real MIME type, rename it randomly, and store it outside the webroot. An upload is attacker-controlled bytes; validate the MIME type, give it your own random name, and keep it out of the public directory.