3.4 · Security: SQL injection, XSS, CSRF, hashing

Level 3 · Databases & OOP

3.4Security: SQL injection, XSS, CSRF, hashing

Goal: protect an application against major flaws (SQL injection, XSS, CSRF), hash passwords and apply the basics (HTTPS, secrets, errors) — a fundamental web-developer skill.
Estimated time: 13 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

The essentials in 30 seconds

  • Never trust inputs: all outside data is suspect — validate, then handle per context (SQL, HTML, email).
  • Three flaws, three reflexes: SQL injection → prepared statements; XSS → htmlspecialchars() on output; CSRF → verified unique token + SameSite.
  • Passwords never in plain text: password_hash() when storing, password_verify() when logging in — no password hardcoded in code.

Running case — Riad audits his shop before going live

Riad, web developer in Constantine, is about to put his shop online: comments displayed raw, order form with no token, client passwords in plain text in the database. He fixes everything: htmlspecialchars() on each output, a unique CSRF token per form verified on submission, password_hash() at registration and password_verify() at login. The audit goes green before opening.

Last episode of the Riad thread — from database to objects to full armouring.

1. SQL injection, XSS, CSRF: mechanisms and defences

Security is permanent, not a final option, around one principle: NEVER trust inputs. (1) SQL injection (seen in 3.1): SQL injected via concatenated data — defence: PDO prepared statements, always. (2) XSS: JavaScript injected that runs in someone else's browser when untrusted data is displayed unescaped (session theft, defacement) — defence: escape on output with htmlspecialchars(), always, for any untrusted data.

(3) CSRF: forcing a logged-in user to perform an action unknowingly via a forged request from another site — defence: CSRF tokens (unique secret value per form, verified on submission) and SameSite cookies. Riad adds a token generated with random_bytes() as a hidden field in each sensitive form and compares it in session before any processing.

2. Hashing, HTTPS, secrets: non-negotiable hygiene

Passwords are stored HASHED, never in plain text: password_hash() at registration (safe algorithm, built-in salt and cost), password_verify() at login. No password in plain text or hardcoded in code, screenshots or messages — non-negotiable.

The rest of hygiene: HTTPS in production (encrypt), secrets out of code (database keys and passwords in environment variables), detailed errors never displayed in production (logged, not disclosed), PHP and dependencies up to date, authorization checked (not just authentication). To learn: the OWASP Top 10, the web-flaw reference.

Common misinterpretation: “Echoing data from my own database is safe, and hashing slows down login.” No. The database holds user-entered data — untrusted by construction, to escape on every output. And password_verify() takes a few milliseconds: an invisible cost to the user, but one making a stolen database unexploitable. Security is not traded for comfort.
The armouring quartet Four doors, four locks Suspect inputs Validatefirst, alwaysSQL + HTML Prepared +htmlspecialcharsForms Verified CSRFtoken + SameSiteAccounts password_hash /verify, HTTPS, secrets
Four doors, four locks
Text version of the diagram (SEO / screen readers / translation): 1) Inputs: Riad treats each outside data as suspect and validates it. 2) SQL and HTML outputs: prepared statements to the database, htmlspecialchars() to the browser. 3) Actions: each form carries a CSRF token verified against the session. 4) Accounts: hashed passwords, HTTPS, environment secrets. Lesson: think security from design, at every layer.

🔎 Concrete example — XSS neutralized, CSRF verified, password hashed

Riad secures a pseudo display, a form and registration.

Hello, ' . htmlspecialchars($pseudo, ENT_QUOTES, 'UTF-8') . '

'; // 2) CSRF: unique token in session, hidden field, verification if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } // in the form: if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) { http_response_code(403); exit('Request denied.'); } // 3) Hashing: never in plain text $hash = password_hash($motDePasseSaisi, PASSWORD_DEFAULT); // ... storing the $hash in the database via prepared query ... $ok = password_verify($motDePasseSaisi, $hashStocke);

Reading: escaping on output, token compared with hash_equals, hashing then verification.

Lesson: three short moves closing three serious flaws.

Section vocabulary

Never trust inputs
Cross-cutting principle: all outside data is potentially malicious — validate then handle per context.

Simple example: Riad validates the email before any use.

What it is not: displaying as-is “because it comes from my form”.

Reflex: suspect by default, validated before use.

SQL injection → prepared
SQL injected via concatenation; defence: always PDO prepared statements, never concatenation.

Simple example: Riad's shop prepare/execute.

What it is not: a “temporary” concatenation.

Reflex: placeholders + execute, no exceptions.

XSS → htmlspecialchars
Injected script executed in other visitors' browsers; defence: escape all untrusted data on output.

Simple example: The commented pseudo displayed as harmless text.

What it is not: a direct echo of a user field.

Reflex: htmlspecialchars() on every untrusted output.

CSRF → tokens
Action forced unknowingly on a logged-in user; defence: unique per-form token verified + SameSite cookie.

Simple example: The hidden field compared in session at Riad's.

What it is not: a sensitive form with no token at all.

Reflex: a token on every sensitive action.

Hashing / HTTPS / secrets
Hashed passwords (password_hash/verify, never plain text); HTTPS; environment secrets; errors hidden in prod; up to date.

Simple example: Riad's hashed registration and verified login.

What it is not: a password stored readably in the database.

Reflex: no secret in plain text, anywhere.

In practice — Securing a PHP application

  1. Against SQL injection: ALWAYS prepared statements (PDO) — check no data is concatenated into SQL.
  2. Against XSS: escape all displayed data with htmlspecialchars() (comments, pseudos, database data).
  3. Against CSRF: add a CSRF token (unique, secret) to your forms and verify it on submission; use SameSite on cookies.
  4. Hash passwords (password_hash/verify), use HTTPS, manage secrets out of code, do not display errors in production, keep PHP/dependencies up to date.
You protect your application against major flaws (SQL injection, XSS, CSRF), hash passwords and apply security basics (HTTPS, secrets, errors) — a fundamental web-developer skill.

Ready-to-say script — presenting your security audit (30 s)

“Before going live: prepared statements everywhere, htmlspecialchars on every output, verified CSRF token on sensitive forms, passwords hashed with password_hash and verified with password_verify. Secrets in environment, HTTPS in production.”

Say in pre-launch review; show the four moves in the code.

Copy-paste template — safe registration and login

prepare('INSERT INTO utilisateurs (email, mot_de_passe) VALUES (:email, :hash)');
$stmt->execute(['email' => $email, 'hash' => $hash]);
// Login: read with prepare, verify the hash
$stmt = $pdo->prepare('SELECT id, mot_de_passe FROM utilisateurs WHERE email = :email');
$stmt->execute(['email' => $emailSaisi]);
$utilisateur = $stmt->fetch();
if ($utilisateur && password_verify($motDePasseSaisi, $utilisateur['mot_de_passe'])) {
  session_regenerate_id(true);
  $_SESSION['utilisateur_id'] = $utilisateur['id'];
}
// Output: always escape
$pseudoAffiche = htmlspecialchars($pseudo, ENT_QUOTES, 'UTF-8');

Independent

Prepared + htmlspecialchars + hashing: the trio is enough to launch with no serious fault.

SME in Constantine

Riad adds CSRF tokens to order slips before opening.

Product team

Enforce the four-move checklist in systematic code review.

Beginner

This week: one escaped output and one hashed registration.

Caution: Security — vital reflexes: NEVER store a password in plain text or hardcoded (neither in the database, nor in code, nor in screenshots): password_hash() when storing, password_verify() when logging in. NEVER concatenate data into SQL (prepared statements) and NEVER output untrusted data without htmlspecialchars(). Work with test accounts and data, keep secrets and keys out of the repo (environment variables), and if exposed (a disclosed password or key), consider it compromised: regenerate it immediately.

✓ Best practices

  • Think security from design, at every layer (input, SQL, HTML, action, storage).
  • Use proven tools (prepared, htmlspecialchars, password_hash, tokens) rather than in-house.
  • Keep learning (OWASP Top 10): attacks evolve.

✗ Common pitfalls

  • Displaying data from “your own database” unescaped (it comes from users).
  • Forgetting the CSRF token on a sensitive action (order, deletion).
  • Displaying detailed errors in production (disclosed information).

Key takeaways

  • SECURITY is PERMANENT, around one principle: NEVER TRUST inputs (all outside is suspect).
  • SQL INJECTION → PREPARED (never concatenation). XSS → ESCAPE with htmlspecialchars(). CSRF → TOKENS + SameSite.
  • HASHED passwords (password_hash/verify, NEVER plain text or hardcoded). HTTPS, SECRETS out of code, errors hidden in prod, UP TO DATE.
  • PROVEN tools + training (OWASP Top 10). An unsecured site endangers its users — professional responsibility.

48h challenge — apply tomorrow

Add 1 verified CSRF token to a form, escape 3 outputs and convert 1 storage to password_hash.

Section exit checklist
  • I recite the four defences without hesitation.
  • No data is concatenated or displayed raw.
  • No password exists in plain text in my code.
Self-rating 1-5: does each output (SQL, HTML, action, storage) have its defence?would an audit find a plain-text secret?

Frequently asked questions

XSS and CSRF: concretely, how to protect against them?

For XSS: an attacker slips JavaScript into a field (comment, pseudo) displayed unescaped — it runs in visitors' browsers (session theft, redirection). Defence: htmlspecialchars() on all untrusted data before output (the script becomes harmless text). For CSRF: a forged request from another site makes a logged-in user act unknowingly (changing an email, ordering). Defence: unique secret token per form, verified on submission (+ SameSite on cookies). Two different mechanisms, two systematic defences.

Where to start with security, and which mindset to adopt?

The mindset: “never trust inputs” — all outside is suspect (even “your” UI, bypassable with direct requests). High-impact fundamentals: (1) SQL injection → prepared, (2) XSS → escape, (3) CSRF → tokens, (4) passwords → hashing, (5) HTTPS, (6) secrets out of code, (7) errors hidden in prod, (8) authorization (rights, not just login), (9) locked-down uploads. Then the OWASP Top 10 to structure what follows.

Check your understanding

A visitor posts a pseudo with JavaScript in it. Correct reflex?

Additional resources (optional, to go further)

Level 3 examFresh, harder questions to test what you've learned