1. Server-Side Request Forgery (SSRF)
SSRF happens when an application fetches a URL supplied (directly or indirectly) by a user, and an attacker points that fetch somewhere the server can reach but the attacker never could directly — the server's own internal network, localhost services, or cloud metadata endpoints.
// app.js -- fetches whatever URL the user supplies, to display as an avatar
app.post('/avatar', async (req, res) => {
const image = await fetch(req.body.imageUrl);
// ...saves the image
});
// Normal use: imageUrl = "https://example.com/photo.jpg"
// Attack: imageUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
// This is the AWS instance metadata endpoint -- reachable ONLY from inside
// the cloud instance itself. The SERVER fetches it on the attacker's behalf,
// potentially returning live IAM credentials in the response.
This is a direct preview of Week 8's cloud security: the metadata endpoint exists so a legitimate application on that instance can retrieve its own temporary AWS credentials — SSRF turns that legitimate feature into a way for an outside attacker to steal them, entirely through a URL the server fetches on the attacker's behalf.
# 1. Allowlist expected domains/protocols -- never fetch arbitrary user-supplied URLs
# 2. Block requests to private IP ranges and the cloud metadata address explicitly:
# 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.169.254
# 3. Disable HTTP redirects when fetching, or re-validate the destination after
# following one -- a redirect can point somewhere the original URL didn't
# 4. If the metadata endpoint must be reachable, require IMDSv2 (a session-token
# based version AWS added specifically because plain GET-based SSRF like
# this was such a common real-world attack path)
Unlike CSRF (Week 6), which exploits a victim's browser and its cookies, SSRF exploits the trust relationship between the server itself and its own internal network — the request genuinely originates from a trusted internal source, because it does. This is exactly why Week 2's network segmentation matters even for internal-only services: "not reachable from the internet directly" doesn't mean "not reachable at all" if a public-facing server can be tricked into forwarding a request on an attacker's behalf.
2. Insecure Deserialization
Serialization turns an in-memory object into a storable/transmittable format (JSON, or a language-specific binary format); deserialization reverses it. Some languages' native serialization formats can reconstruct arbitrary objects — including ones whose constructor or destructor runs code — which means deserializing untrusted data can mean executing untrusted code.
# In languages with "rich" native serialization (Java, PHP, Python's pickle,
# Ruby's Marshal), deserializing a byte stream can reconstruct ANY object
# the language's type system allows -- including objects whose creation
# triggers side effects.
# An attacker who can supply the serialized data (a cookie, a cache value,
# an API payload) can craft one that, when deserialized, executes arbitrary
# code -- this is a well-documented, repeated real-world vulnerability class
# in Java and PHP applications specifically.
# Plain JSON deserialization does NOT have this problem -- JSON.parse()
# only ever produces plain objects/arrays/primitives, never arbitrary
# class instances with executable side effects.
The practical guidance is simple: prefer JSON (or another data-only format) for anything crossing a trust boundary, and if a language's native "rich" serialization format is unavoidable, never deserialize data from an untrusted source without strict type allowlisting.
pickle module says this out loud, in its own docs
Python's official pickle documentation explicitly warns: "never unpickle data received from an untrusted or unauthenticated source." It's one of the rare cases where the vulnerability is documented plainly by the tool itself — worth remembering the next time pickle.loads() shows up on data that didn't originate entirely within your own trusted system.
3. Broken Access Control
Week 5 introduced the principle: authorization must be checked server-side, on every request. Broken access control is what happens when it isn't — and the most common concrete form is IDOR (Insecure Direct Object Reference): an endpoint that trusts a client-supplied ID without confirming the requester actually owns or is allowed to access that specific resource.
// GET /api/invoices/8842 -- returns alice's own invoice, fine
// GET /api/invoices/8843 -- alice tries the next ID, and gets bob's invoice
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findById(req.params.id);
res.json(invoice); // BUG: never checked that req.user owns this invoice
});
// Fix: verify ownership as part of every fetch, not just at a higher "you're logged in" gate
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findById(req.params.id);
if (!invoice || invoice.userId !== req.user.id) {
return res.status(404).send(); // 404, not 403 -- don't confirm the ID even exists
}
res.json(invoice);
});
IDOR is common precisely because it's easy to get "authenticated" right (you must be logged in) while forgetting "authorized for this specific resource" (you must own this invoice) — the two are different checks, and a system that only enforces the first has broken access control even though login itself works perfectly.
Switching from sequential integer IDs to UUIDs makes IDOR harder to discover by guessing, but doesn't fix the underlying missing check — a UUID leaked in a URL, a log, or a Referer header is just as exploitable as a guessable integer once an attacker has it. The actual fix is always the server-side ownership check, never obscuring the identifier.
4. Security Misconfiguration
A perfectly secure codebase can still ship a vulnerable application if the environment around it is misconfigured — this category covers everything that's wrong not because of a coding mistake, but a deployment or default-settings one.
1. Default credentials left unchanged (admin/admin on a database console)
2. Verbose error pages in production, leaking stack traces, file paths,
or database schema to any user who triggers an error
3. Directory listing enabled on a web server, exposing files never meant
to be browsed to directly
4. Unnecessary features/services/ports enabled (Week 3's attack surface,
applied specifically to the app layer -- an admin panel with no
additional auth, reachable from the public internet)
5. Overly permissive CORS (Access-Control-Allow-Origin: *) on an endpoint
that returns sensitive, authenticated data
6. Outdated software with known CVEs left unpatched (Week 11)
// An API endpoint returning private user data
app.get('/api/me', (req, res) => {
res.set('Access-Control-Allow-Origin', '*'); // any site can read this response
res.set('Access-Control-Allow-Credentials', 'true'); // WITH the user's cookies attached
res.json(userData);
});
// Any malicious site the victim visits can now fetch their private data
// cross-origin, because the server explicitly told every browser it's fine to
Almost every item in this category is caught by the exact discipline Week 3 built for servers, applied to the application layer: a defined, minimal baseline, checked systematically before every deploy — not relying on remembering to disable directory listing manually, every single time, on every single environment.
5. Finding These Live with Burp Suite / OWASP ZAP
An intercepting proxy sits between your browser and the target application, letting you see, pause, and modify every request before it's sent — the single most useful tool for actually finding the vulnerabilities this week and last week covered, rather than just reading about them.
# 1. Start ZAP, configure your browser to proxy through it (usually 127.0.0.1:8080)
# 2. Browse the target app normally -- ZAP passively records every request/response
# 3. Right-click any request -> "Open/Resend with Request Editor" to manually
# modify it: change a parameter, an ID, a cookie -- and resend
# 4. Run ZAP's "Active Scan" against the target to automatically probe for
# common issues (reflected XSS, SQL injection patterns, missing security headers)
For the IDOR from Section 3: intercept a request for your own resource, change the ID in the request editor to a neighboring one, and resend — if you get back data that isn't yours, you've just found (and confirmed) a real broken-access-control vulnerability, by hand, the same way a real attacker would.
An active scan sends genuinely aggressive, exploit-shaped traffic — running one against a service you don't own or don't have explicit written permission to test crosses the exact legal line Week 14 covers formally. This week's exercise, like every hands-on exercise involving exploitation in this course, targets an application you built or run entirely yourself.
6. Hands-on Exercise
Run OWASP ZAP against OWASP Juice Shop, and find an IDOR by hand
Use a real, deliberately vulnerable target app so you're finding genuine issues, not simulated ones.
Part 1 — Set up and run a scan:
- Run OWASP Juice Shop locally (its Docker image is the fastest path:
docker run -p 3000:3000 bkimminich/juice-shop) — a deliberately vulnerable e-commerce app built exactly for this kind of exercise. - Install OWASP ZAP, configure your browser to proxy through it, and browse Juice Shop normally for a few minutes so ZAP records the app's structure.
- Run ZAP's Active Scan against the running app and review the findings report — pick three findings and, for each, note which category from this week or last week it falls into.
Automated scanners produce false positives — before trusting any finding, try to manually reproduce it (Part 2's technique) to confirm it's real. A scanner report is a lead to investigate, not a verified vulnerability list on its own.
Part 2 — Find an IDOR by hand:
- Register two separate accounts in Juice Shop. Log in as the first, and find a request that references your own order or basket by an ID (check your basket, then look at the request in ZAP's history).
- Using ZAP's request editor, resend that exact request but change the ID to a value you'd expect to belong to the second account (or simply an adjacent number).
- Confirm whether you receive the other account's data back — document what you found: the endpoint, the request you sent, and the response.
- Write the server-side fix (in the style of Section 3's example) that would close this specific finding, referencing the actual endpoint and ID you tested.
Juice Shop has an in-app "Score Board" that tracks which of its intentional challenges you've solved — check it after this exercise. If you found a real basket/order IDOR, there's a good chance it's a listed challenge, which is a solid way to confirm you found the intended issue and not an unrelated bug.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is the cloud metadata endpoint (169.254.169.254) such a common target for SSRF specifically?
Why is the cloud metadata endpoint (169.254.169.254) such a common target for SSRF specifically?
It's only reachable from inside the cloud instance itself, and it can return that instance's live IAM credentials — normally a safe design, since an outside attacker can't reach it directly. SSRF breaks that assumption by tricking the server into making the request on the attacker's behalf, turning an internal-only convenience into a credential-theft vector from outside.
Q2
Why is JSON.parse() generally safe from insecure deserialization, while a language's native "rich" serialization format can be dangerous?
Why is JSON.parse() generally safe from insecure deserialization, while a language's native "rich" serialization format can be dangerous?
JSON can only represent plain data — objects, arrays, strings, numbers, booleans, null — with no concept of reconstructing an arbitrary class instance. A rich native format (Java serialization, PHP's, Python's pickle) can reconstruct any object the language allows, including ones whose construction triggers executable side effects, which is exactly what a malicious payload exploits.
Q3
An app switches from sequential integer IDs to random UUIDs for its invoice URLs, but doesn't add an ownership check. Is the IDOR fixed?
An app switches from sequential integer IDs to random UUIDs for its invoice URLs, but doesn't add an ownership check. Is the IDOR fixed?
No. UUIDs make the ID harder to guess by brute force, but any UUID an attacker legitimately obtains (a leaked URL, a shared link, a Referer header) is just as exploitable as a guessable integer, since the server still never checks that the requester actually owns the resource. Obscuring the identifier is not the same as authorizing the request.
Q4
An API sets Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true on an endpoint returning private user data. What's wrong with this combination?
An API sets Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true on an endpoint returning private user data. What's wrong with this combination?
It tells every browser that any website is allowed to read this endpoint's response, with the user's credentials/cookies attached — meaning a malicious site the victim visits can fetch their private data cross-origin, with their own browser doing the work using their real, authenticated session. Access-Control-Allow-Origin should be scoped to specific, trusted origins whenever credentials are involved, never a wildcard.