Security Advanced
By the end of this lesson you'll harden a PHP app the way production teams do — sending the right security headers, encrypting data at rest, verifying JWTs safely, blocking SSRF and deserialization attacks, and auditing your dependencies before they ship.
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️⃣ Security Headers & Content-Security-Policy
A security header is a short instruction you attach to a response that tells the browser how to behave — and it's free, server-side defence the attacker can't remove. The most powerful one is Content-Security-Policy (CSP) : a whitelist of where scripts, styles, and images may load from. With script-src 'self' , even if an attacker injects a script the browser simply refuses to run it. HSTS forces every visit over HTTPS, X-Frame-Options: DENY stops your page being framed for clickjacking, and nosniff stops the browser guessing file types. One catch: headers must be sent before any output, so call them at the very top.
In a real request you'd swap each echo for the commented-out header(...) call. Send these once, near the start of your app, and every page inherits them.
2️⃣ Encryption at Rest & SSRF
Encryption at rest means scrambling sensitive data before it lands on disk, so a stolen database backup is useless without the key. Use the modern sodium extension (built into PHP since 7.2) — it picks safe defaults so you can't accidentally choose a broken cipher like ECB in raw openssl . The same code also shows SSRF (Server-Side Request Forgery): if user input decides which URL your server fetches, an attacker can point it at internal addresses like the cloud-metadata endpoint 169.254.169.254 and steal credentials. The fix is the same whitelist pattern you use everywhere — only allow hosts you trust.
Notice the ciphertext is unreadable bytes, yet decryption with the same key and nonce returns the original exactly. And the server flatly refuses to fetch from 169.254.169.254 because it isn't on the allow-list.
3️⃣ JWT Pitfalls: Algorithm Confusion & Expiry
A JWT (JSON Web Token) is signed JSON used to prove who a user is. The danger is that the token itself claims which algorithm signed it, and naive code trusts that claim. In an alg confusion attack the attacker sets alg to "none" (no signature needed!) or swaps RS256 for HS256 to forge a valid signature. The fixes are simple: never read the algorithm from the token — demand the one you chose — and always check the exp expiry , or a stolen token works forever.
4️⃣ Secrets Management & Secure File Handling
Secrets — database passwords, API keys, encryption keys — must never live in your code or in Git. Keep them in environment variables , often loaded from a .env file that is in .gitignore , and read them with getenv() or $_ENV . A file upload is just attacker-controlled bytes with an attacker-chosen name, so three rules apply: validate the content type, cap the size, and generate your own random filename stored outside the web root — never reuse the client's name (think ../../shell.php ).
The random filename in the output will differ each run (it comes from random_bytes ) — what matters is that the PHP upload is rejected outright and the image is stored under a name the attacker never controlled.
5️⃣ Your Turn
Time to defend something yourself. Each script below is almost complete — fill in every ___ using the 👉 hint, then run it and check it against the Output panel.
One more — and this one is the real bug that defeats forged tokens. The verifier checks expiry but forgets to pin the algorithm, so an alg:"none" token gets in. Add the missing guard.
📋 Quick Reference — Security Headers
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 exact whitelist pattern that stops a server being tricked into fetching internal addresses.
Practice quiz
Which security header is the single best defence against XSS?
- X-Frame-Options
- Referrer-Policy
- Content-Security-Policy
- X-Content-Type-Options
Answer: Content-Security-Policy. A CSP like "script-src 'self'" tells the browser to refuse injected scripts, blocking XSS even if a <script> gets into the page.
What does the X-Frame-Options: DENY header protect against?
- Clickjacking via framing your page in an <iframe>
- MIME-type sniffing
- SQL injection
- Weak passwords
Answer: Clickjacking via framing your page in an <iframe>. DENY stops the page from being embedded in a frame, which is how clickjacking attacks trick users into clicking.
Why must security headers be sent before any echo or HTML output?
- Headers are slower than output
- PHP forbids headers in functions
- Browsers ignore late headers but still run them
- Once the response body starts, headers are already on their way and can't be changed
Answer: Once the response body starts, headers are already on their way and can't be changed. You get 'Cannot modify header information - headers already sent' if output (even a stray space) precedes header() calls.
What is the right tool for two-way encryption of data at rest in modern PHP?
- md5()
- sodium_crypto_secretbox()
- base64_encode()
- password_hash()
Answer: sodium_crypto_secretbox(). The sodium extension (built into PHP 7.2+) picks safe defaults; secretbox encrypts and can decrypt with the same key and nonce.
What is the key difference between hashing and encryption?
- Hashing is one-way (passwords); encryption is two-way (data you must read back)
- Hashing is two-way, encryption is one-way
- They are the same thing
- Encryption is only for passwords
Answer: Hashing is one-way (passwords); encryption is two-way (data you must read back). Use password_hash (one-way) for passwords you only compare, and sodium encryption (two-way) for data like a stored SSN you must decrypt.
How do you defend against SSRF when the server fetches a URL?
- Trust any https:// URL
- Use file_get_contents directly on user input
- Allow-list permitted hosts and block private/loopback addresses
- Only fetch URLs over HTTP
Answer: Allow-list permitted hosts and block private/loopback addresses. Never let user input pick the URL freely; allow-list the hosts you trust so attackers can't reach 169.254.169.254 or internal services.
What are the two classic JWT 'alg confusion' attacks?
- Expired tokens and missing claims
- alg:"none" (no signature) and swapping RS256 for HS256
- Base64 and hex encoding mismatches
- Wrong issuer and wrong audience
Answer: alg:"none" (no signature) and swapping RS256 for HS256. Never trust the token's alg field: attackers set it to none or swap algorithms. Pin the algorithm you expect and reject anything else.
Besides pinning the algorithm, what must you always check on a JWT?
- The token's length
- That it is uppercase
- The number of dots in the token
- The 'exp' expiry claim
Answer: The 'exp' expiry claim. A token without an expiry check is valid forever; always verify exp so a stolen token eventually stops working.
Why should you never call unserialize() on user input?
- It is slower than json_decode
- It can rebuild objects and fire magic methods, enabling object-injection gadget chains
- It only works on arrays
- It strips all whitespace
Answer: It can rebuild objects and fire magic methods, enabling object-injection gadget chains. unserialize() can instantiate objects and run __wakeup/__destruct; use json_decode(), which only ever produces plain arrays and scalars.
What does 'composer audit' do?
- Reformats your composer.json
- Runs your unit tests
- Scans installed package versions against known security advisories
- Deletes unused dependencies
Answer: Scans installed package versions against known security advisories. composer audit compares composer.lock versions to a vulnerability database and flags dependencies with known advisories.