Building Real ApplicationsExtra· 45 min read

Web Security: XSS, CSRF & Passwords

The security baseline every employer expects — escaping output to stop XSS, CSRF tokens to stop forged requests, and hashing passwords the right way.

What you will learn

  • Prevent cross-site scripting (XSS) by escaping output
  • Protect forms with CSRF tokens
  • Hash and verify passwords with passwordhash / passwordverify

Security is not optional

You already know about SQL injection (use prepared statements). But employers expect three more defences as a baseline, and missing any one of them can sink a real site. This lesson covers the big three: XSS, CSRF and password hashing. None are hard — they are just habits you apply every time.

XSS — escape everything you output

XSS (cross-site scripting) is when an attacker sneaks <script> into your page — say, by typing it into a comment box — and it then runs in other visitors’ browsers, stealing their data. The defence is simple and absolute: escape any user-supplied text before printing it, so tags become harmless characters. htmlspecialchars() does exactly this.

Escaping output to stop XSS
<?php
  // imagine a comment a user typed:
  $comment = '<script>alert("hacked")</script>';

  echo "Unsafe: " . $comment;                       // DANGER
  echo "<br>Safe: " . htmlspecialchars($comment);   // neutralised
?>

The first echo prints the comment as-is — the browser would treat <script> as real code and run it. The second wraps it in htmlspecialchars(), which converts < into &lt;, > into &gt; and so on, so the browser shows the text literally instead of executing it. The script becomes visible, harmless text.

Note: Output (in the browser): Unsafe: (an alert box pops up — the attack ran!) Safe: <script>alert("hacked")</script> The safe line displays the tags as plain text; nothing runs. The rule is mechanical: every piece of user data you echo into a page gets wrapped in htmlspecialchars(). No exceptions.

CSRF — prove the request came from your form

CSRF (cross-site request forgery) is when another site tricks a logged-in user’s browser into silently submitting a form to *your* site — like a hidden "delete my account" request. The defence is a CSRF token: a secret random value your form includes and your server checks. A forged request from another site cannot know the token, so it is rejected.

It works in three steps:

  1. When you show the form, generate a random token, store it in the session, and put it in a hidden field.
  2. The browser sends the token back with the form.
  3. On submit, compare the token from the form with the one in the session — if they do not match, reject the request.
Put a CSRF token in the form
<?php
  session_start();

  // when SHOWING the form: make and remember a token
  $_SESSION["csrf"] = bin2hex(random_bytes(32));
?>
<form action="delete.php" method="post">
  <input type="hidden" name="csrf" value="<?= $_SESSION["csrf"] ?>">
  <button>Delete account</button>
</form>

bin2hex(random_bytes(32)) creates a long, unguessable random token; we save it in $_SESSION["csrf"] and also drop it into a hidden form field. The user never sees it, but it travels back when they submit — proving the request really came from a page *you* served.

Verify the token before acting
<?php
  // delete.php — when RECEIVING the form
  session_start();

  if (($_POST["csrf"] ?? "") !== ($_SESSION["csrf"] ?? "_")) {
    http_response_code(403);
    exit("Invalid request.");
  }

  // token matched — safe to proceed
  echo "Account deleted.";
?>

On submit, we compare the token the form sent ($_POST["csrf"]) with the one we stored in the session. If they differ, the request did not come from our form — we send a 403 "forbidden" and stop. Only a matching token lets the dangerous action proceed.

Note: Output (a normal user clicking the button): Account deleted. A forged request from a malicious site would carry no valid token, so the comparison fails and it sees Invalid request. instead. This is exactly what Laravel’s automatic @csrf does for you behind the scenes.

Passwords — hash, never store plain

You must never store a password as plain text — if your database leaks, every account is exposed. Instead you store a hash: a scrambled, one-way version that cannot be reversed back to the password. PHP makes this safe and easy with password_hash() to scramble and password_verify() to check a login.

Hash on signup, verify on login
<?php
  // when the user SIGNS UP: store the hash, not the password
  $password = "secret123";
  $hash = password_hash($password, PASSWORD_DEFAULT);
  // save $hash in the database

  // when the user LOGS IN: verify the typed password against the hash
  $typed = "secret123";
  if (password_verify($typed, $hash)) {
    echo "Login successful";
  } else {
    echo "Wrong password";
  }
?>

On signup, password_hash($password, PASSWORD_DEFAULT) turns the password into a long hash (it also adds random "salt" automatically, so identical passwords hash differently) — and that is what you store, never the password itself. On login, password_verify($typed, $hash) checks whether the typed password matches the stored hash, returning true or false. You never decrypt anything, because a hash cannot be undone.

Note: Output (in the browser): Login successful The stored hash looks like $2y$10$N9qo8uLOickgx2ZMRZoMy... — unreadable and irreversible. Even if attackers steal your database, they cannot recover the original passwords. This is the only acceptable way to handle passwords.

Watch out: Never invent your own password scrambling (md5, sha1, "reverse the letters") — they are all broken. Always use password_hash() / password_verify(), which use strong, modern algorithms and are updated by PHP over time.

Q. How should you store user passwords?

Answer: Store a one-way hash from passwordhash() and check logins with passwordverify(). Hashes cannot be reversed, so a leaked database does not expose the real passwords.

✍️ Practice

  1. Echo a user comment safely with htmlspecialchars and confirm a <script> tag does not run.
  2. Hash a password with password_hash and verify it with password_verify.

🏠 Homework

  1. Add a CSRF token to a form (generate, hide, verify on submit) and reject any submission with a missing or wrong token.
Want to learn this with a mentor?

CodingClave runs guided, project-based training (28-day, 45-day & 6-month batches).

Explore Training →