{"lessons":{"access-control":{"id":"access-control","title":"Object Access Control (IDOR)","unit":3,"branch":"web","difficulty":2,"prereqs":["session-management"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Object Access Control (IDOR)</h1>\n<h2>Concept</h2>\n<p>Now the user is properly logged in; the question is what they&#x27;re allowed to reach. A broken access control, often called IDOR, is when the app checks that you&#x27;re logged in but not that the thing you&#x27;re asking for is yours. It&#x27;s currently the number one risk on the OWASP Top 10.</p>\n<h2>Example</h2>\n<p>A logged in user opens <code>/invoice?id=105</code> and sees their own invoice. They change it to <code>/invoice?id=106</code> and see a stranger&#x27;s, because the server looked up the invoice by ID without checking who owns it.</p>\n<h2>Debrief</h2>\n<p>Every request for a specific record must verify the current user is allowed to see that record, server side. Hiding the admin link in the menu does nothing, because the URL can be typed directly.</p>\n<h2>Lab</h2>\n<p>Two steps. Change an <code>?id=</code> value to read another user&#x27;s invoice, then request an <code>/admin</code> page that has no role check at all. The flag sits on the admin page.</p>","short_html":"<h2>Concept</h2>\n<p>Now the user is properly logged in; the question is what they&#x27;re allowed to reach. A broken access control, often called IDOR, is when the app checks that you&#x27;re logged in but not that the thing you&#x27;re asking for is yours. It&#x27;s currently the number one risk on the OWASP Top 10.</p>\n<h2>Debrief</h2>\n<p>Every request for a specific record must verify the current user is allowed to see that record, server side. Hiding the admin link in the menu does nothing, because the URL can be typed directly.</p>","quiz":[{"q":"A user changes /invoice?id=105 to another id and sees a stranger's invoice. The missing control is:","options":["Input validation on the id","A check that the invoice belongs to the current user","HTTPS on the endpoint","Rate limiting"]},{"q":"Why doesn't hiding the admin link in the UI protect /admin?","options":["The user can request the URL directly regardless of the UI","The link is cached by the browser","Search engines index the link","The CSS can be disabled"]}],"lab":{"type":"container","image":"cyberforge/access-control","objective":"Read another user's invoice by id, then reach the unprotected admin page","needs_flag":true,"needs_code":false}},"authentication":{"id":"authentication","title":"Authentication Boundaries","unit":3,"branch":"web","difficulty":2,"prereqs":["cookies-sop","untrusted-input"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Authentication Boundaries</h1>\n<h2>Concept</h2>\n<p>Authentication is proving who you are, usually with a password. Its weak points aren&#x27;t exotic: leaky error messages that reveal valid usernames, no limit on guesses, and weak or reused passwords. Together they let an attacker find a real account and guess their way in.</p>\n<h2>Example</h2>\n<p>A login that says &quot;user not found&quot; for one input and &quot;wrong password&quot; for another is quietly telling an attacker which usernames exist. Once they have a valid username and there&#x27;s no rate limit, they can try thousands of passwords until one works.</p>\n<h2>Debrief</h2>\n<p>Use one generic failure message for both cases, rate limit and lock out after repeated failures, and store passwords with a slow salted hash like bcrypt so a stolen database isn&#x27;t instantly crackable.</p>\n<h2>Lab</h2>\n<p>First use the differing error messages to confirm a valid username, then brute force its password against a small supplied wordlist. Logging in reveals the flag. Rate limiting is deliberately off so the attack is visible; the debrief points out that fixing it is the defense.</p>","short_html":"<h2>Concept</h2>\n<p>Authentication is proving who you are, usually with a password. Its weak points aren&#x27;t exotic: leaky error messages that reveal valid usernames, no limit on guesses, and weak or reused passwords. Together they let an attacker find a real account and guess their way in.</p>\n<h2>Debrief</h2>\n<p>Use one generic failure message for both cases, rate limit and lock out after repeated failures, and store passwords with a slow salted hash like bcrypt so a stolen database isn&#x27;t instantly crackable.</p>","quiz":[{"q":"Why is a login that says 'user not found' vs 'wrong password' a problem?","options":["It lets an attacker confirm which usernames exist","It leaks the password hash","It disables rate limiting","It exposes the session cookie"]},{"q":"Which most directly stops online password brute forcing?","options":["Longer password hashes","HTTPS on the login page","Rate limiting and lockout after failed attempts","A CAPTCHA on the signup page"]}],"lab":{"type":"container","image":"cyberforge/authentication","objective":"Enumerate a valid username, then brute force its password","needs_flag":true,"needs_code":false}},"capstone-chain":{"id":"capstone-chain","title":"Chained Attack","unit":6,"branch":"web","difficulty":5,"prereqs":["access-control","xss","csrf"],"lab_type":"container","has_quiz":false,"body_html":"<h1>Chained Attack</h1>\n<h2>Concept</h2>\n<p>Real attacks are rarely one trick; they&#x27;re a chain where each step opens the next. The capstone drops you into one vulnerable app with a single goal and no hand holding, and only a chain of earlier skills gets you there. It proves you can connect lessons, not just pass them one at a time.</p>\n<h2>Example</h2>\n<p>A possible path through the box: use IDOR to enumerate users and find an admin account, plant a stored XSS payload that steals the admin&#x27;s session when their bot views it, then use that session to reach an admin only page holding the final flag. You may find other valid chains.</p>\n<h2>Debrief</h2>\n<p>Walk back through the chain and name the one fix at each step that would have broken it: an ownership check, output encoding, a role check on the admin page. Defense in depth means an attacker has to beat all of them, not just one.</p>\n<h2>Lab</h2>\n<p>One container, one final flag, unlocked only after Units 1-4 are complete. No step by step objective is given on purpose; a hint system can reveal the next stage if you&#x27;re stuck.</p>","short_html":"<h2>Concept</h2>\n<p>Real attacks are rarely one trick; they&#x27;re a chain where each step opens the next. The capstone drops you into one vulnerable app with a single goal and no hand holding, and only a chain of earlier skills gets you there. It proves you can connect lessons, not just pass them one at a time.</p>\n<h2>Debrief</h2>\n<p>Walk back through the chain and name the one fix at each step that would have broken it: an ownership check, output encoding, a role check on the admin page. Defense in depth means an attacker has to beat all of them, not just one.</p>","quiz":[],"lab":{"type":"container","image":"cyberforge/capstone-chain","objective":"Chain IDOR, XSS, and session takeover to reach the admin flag","hints":true,"needs_flag":true,"needs_code":false}},"command-injection":{"id":"command-injection","title":"Command Injection","unit":2,"branch":"web","difficulty":3,"prereqs":["sqli"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Command Injection</h1>\n<h2>Concept</h2>\n<p>Same root cause as SQLi, different target: instead of a database, the input reaches the operating system shell. If an app builds a shell command out of user input, an attacker can tack on commands of their own.</p>\n<h2>Example</h2>\n<p>A network tool runs <code>os.system(&quot;ping &quot; + host)</code>. Sending a host value with a <code>;</code> followed by another command makes the shell run <code>ping</code>, then run the attacker&#x27;s command, because <code>;</code> chains commands.</p>\n<h2>Debrief</h2>\n<p>Don&#x27;t call the shell with a built string. Pass arguments as a list straight to the program (<code>subprocess.run([&quot;ping&quot;, host])</code>), which removes the shell parsing that makes injection possible. Blocklisting characters leaks.</p>\n<h2>Lab</h2>\n<p>The container has a &quot;ping a host&quot; page. Inject a second command to read <code>flag.txt</code> from the server&#x27;s filesystem. Because real shell access is possible, this lab needs the hardened container (no network out, unprivileged, read only except a scratch dir).</p>","short_html":"<h2>Concept</h2>\n<p>Same root cause as SQLi, different target: instead of a database, the input reaches the operating system shell. If an app builds a shell command out of user input, an attacker can tack on commands of their own.</p>\n<h2>Debrief</h2>\n<p>Don&#x27;t call the shell with a built string. Pass arguments as a list straight to the program (<code>subprocess.run([&quot;ping&quot;, host])</code>), which removes the shell parsing that makes injection possible. Blocklisting characters leaks.</p>","quiz":[{"q":"Code runs os.system(\"ping \" + host). What does a host of  8.8.8.8; cat /etc/passwd  do?","options":["Runs ping, then runs cat /etc/passwd","Pings an invalid host and errors","Is blocked because it contains a space","Pings a host literally named 8.8.8.8;cat"]},{"q":"The safest fix for the ping tool is:","options":["Remove semicolons from the input","Block the words cat and rm","Pass arguments as a list to the program, with no shell","Run the command as a unprivileged user"]}],"lab":{"type":"container","image":"cyberforge/command-injection","objective":"Chain a command onto the ping tool to read flag.txt","needs_flag":true,"needs_code":false}},"cookies-sop":{"id":"cookies-sop","title":"Cookies, Sessions & Same Origin Policy","unit":1,"branch":"web","difficulty":2,"prereqs":["http-basics"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Cookies, Sessions &amp; Same Origin Policy</h1>\n<h2>Concept</h2>\n<p>HTTP has no memory, so sites use cookies to remember you between requests. After login the server gives you a session cookie, and your browser sends it back on every request so the server knows who you are. The Same Origin Policy is the rule that keeps one site&#x27;s pages from reading another site&#x27;s data in your browser. These two ideas underpin XSS, CSRF, and session attacks, so they come before all of them.</p>\n<h2>Example</h2>\n<p>Script on <code>evil.com</code> can send a request to <code>bank.com</code>, and your bank cookie goes along automatically, but the Same Origin Policy stops <code>evil.com</code> from reading the response. That one way gap is exactly why CSRF works and why stealing data cross origin usually doesn&#x27;t.</p>\n<h2>Debrief</h2>\n<p>Protect session cookies with flags: <code>HttpOnly</code> hides them from JavaScript, <code>Secure</code> keeps them off plain HTTP, and <code>SameSite</code> limits when they&#x27;re sent to other sites. Each one blocks a different attack you&#x27;ll meet later.</p>\n<h2>Lab</h2>\n<p>The app stores the user&#x27;s role in a cookie like <code>role=user</code>. Change it to <code>role=admin</code>, resend the request, and unlock an admin page holding the flag. It shows why trusting a client side value is dangerous.</p>","short_html":"<h2>Concept</h2>\n<p>HTTP has no memory, so sites use cookies to remember you between requests. After login the server gives you a session cookie, and your browser sends it back on every request so the server knows who you are. The Same Origin Policy is the rule that keeps one site&#x27;s pages from reading another site&#x27;s data in your browser. These two ideas underpin XSS, CSRF, and session attacks, so they come before all of them.</p>\n<h2>Debrief</h2>\n<p>Protect session cookies with flags: <code>HttpOnly</code> hides them from JavaScript, <code>Secure</code> keeps them off plain HTTP, and <code>SameSite</code> limits when they&#x27;re sent to other sites. Each one blocks a different attack you&#x27;ll meet later.</p>","quiz":[{"q":"Script on evil.com calls fetch('https://bank.com/balance'). By default:","options":["The browser blocks the request from being sent","The request is sent, but evil.com cannot read the response","The request is sent and the response is readable","The browser prompts the user"]},{"q":"What does the HttpOnly flag on a session cookie stop?","options":["The cookie being sent over plain HTTP","JavaScript reading the cookie via document.cookie","The cookie being sent to other sites","The cookie from ever expiring"]}],"lab":{"type":"container","image":"cyberforge/cookies-sop","objective":"Edit the role cookie to reach the admin page","needs_flag":true,"needs_code":false}},"csrf":{"id":"csrf","title":"Request Forgery Defenses","unit":4,"branch":"web","difficulty":3,"prereqs":["session-management","cookies-sop"],"lab_type":"container+code","has_quiz":true,"body_html":"<h1>Request Forgery Defenses</h1>\n<h2>Concept</h2>\n<p>CSRF abuses the fact that the browser automatically attaches your cookies to any request to a site, even one triggered by a different site. So a malicious page can make your browser send a real, authenticated request to a site you&#x27;re logged into, without the attacker ever seeing the response.</p>\n<h2>Example</h2>\n<p>You&#x27;re logged into your bank. You visit a malicious page that submits automatically a hidden form to <code>bank.com/transfer</code>. Your bank cookie rides along, the bank sees a valid logged in request, and the transfer goes through. The Same Origin Policy stops the attacker reading the reply, but the action already happened.</p>\n<h2>Debrief</h2>\n<p>Defend with a CSRF token: a secret, per session random value the real form includes and the attacker can&#x27;t know. <code>SameSite</code> cookies are a strong complementary defense, since they stop the cookie being sent on cross site requests.</p>\n<h2>Lab</h2>\n<p>Two parts. First (container), build an attacker page that makes a logged in bot change its account email, proving the forgery works. Second (code), add a CSRF token to the form and its server check; the test confirms the attack page now fails while the legitimate form still works.</p>","short_html":"<h2>Concept</h2>\n<p>CSRF abuses the fact that the browser automatically attaches your cookies to any request to a site, even one triggered by a different site. So a malicious page can make your browser send a real, authenticated request to a site you&#x27;re logged into, without the attacker ever seeing the response.</p>\n<h2>Debrief</h2>\n<p>Defend with a CSRF token: a secret, per session random value the real form includes and the attacker can&#x27;t know. <code>SameSite</code> cookies are a strong complementary defense, since they stop the cookie being sent on cross site requests.</p>","quiz":[{"q":"CSRF works because the browser automatically includes what with a cross site request?","options":["The victim's cookies for the target site","The victim's password","The attacker's cookies","The page's CSP header"]},{"q":"Which best stops CSRF?","options":["Checking the user is logged in","Using HTTPS","A per request CSRF token the attacker can't know, or SameSite cookies","Hiding the form's action URL"]}],"lab":{"type":"container+code","image":"cyberforge/csrf","starter_file":"account.py","objective":"Forge an email change request, then defend it with a CSRF token","tests":["attack_blocked: forged request without token fails","legit_form_works: real form still succeeds"],"needs_flag":true,"needs_code":true}},"http-basics":{"id":"http-basics","title":"HTTP Request & Response","unit":1,"branch":"web","difficulty":1,"prereqs":["web-foundations"],"lab_type":"container","has_quiz":true,"body_html":"<h1>HTTP Request &amp; Response</h1>\n<h2>Concept</h2>\n<p>HTTP is the format of every request and reply. A request has a method (<code>GET</code>, <code>POST</code>, and others), a path, headers (extra info like cookies and content type), and sometimes a body. A response has a status code (200 OK, 302 redirect, 404 not found), its own headers, and a body. Every attack later in this module is just a carefully written HTTP request, so you need to read and write them comfortably.</p>\n<h2>Example</h2>\n<p>After a successful login the server might reply <code>302 Found</code> with <code>Location: /dashboard</code> and <code>Set-Cookie: session=abc</code>. The browser stores the cookie and automatically sends <code>GET /dashboard</code> with it attached. Nothing here is magic; it is all visible, and all editable, in an intercepting proxy.</p>\n<h2>Debrief</h2>\n<p>Any part of a request can be changed by the sender: method, path, headers, cookies, hidden fields, body. Treat all of it as attacker controlled when you design a server.</p>\n<h2>Lab</h2>\n<p>Read a flag out of a custom response header, then send a request with a method the web form never offers (for example <code>PUT</code> or <code>DELETE</code>) to reveal a second flag. It forces you to build a request by hand rather than just clicking.</p>","short_html":"<h2>Concept</h2>\n<p>HTTP is the format of every request and reply. A request has a method (<code>GET</code>, <code>POST</code>, and others), a path, headers (extra info like cookies and content type), and sometimes a body. A response has a status code (200 OK, 302 redirect, 404 not found), its own headers, and a body. Every attack later in this module is just a carefully written HTTP request, so you need to read and write them comfortably.</p>\n<h2>Debrief</h2>\n<p>Any part of a request can be changed by the sender: method, path, headers, cookies, hidden fields, body. Treat all of it as attacker controlled when you design a server.</p>","quiz":[{"q":"A response is `302` with `Location: /dashboard` and `Set-Cookie: session=abc`. The browser will:","options":["Show an error because 302 is not 200","Store the cookie and send GET /dashboard with it","Ignore the cookie until the next refresh","Resend the original POST to /dashboard"]},{"q":"Which parts of an HTTP request can the sender change with a proxy?","options":["Only visible form fields","Form fields and the URL, but not headers","Everything: method, path, headers, cookies, and body","Nothing once the page has loaded"]}],"lab":{"type":"container","image":"cyberforge/http-basics","objective":"Read the flag from a response header, then send an unlisted HTTP method","needs_flag":true,"needs_code":false}},"output-encoding-csp":{"id":"output-encoding-csp","title":"Output Encoding & CSP","unit":4,"branch":"web","difficulty":3,"prereqs":["xss"],"lab_type":"code","has_quiz":true,"body_html":"<h1>Output Encoding &amp; CSP</h1>\n<h2>Concept</h2>\n<p>The main defense against XSS is output encoding: when you put user data into a page, convert characters like <code>&lt;</code> and <code>&gt;</code> into harmless entities so the browser displays them instead of running them. A Content Security Policy is a second layer that tells the browser which scripts are allowed to run at all, so a missed spot is less likely to be exploitable.</p>\n<h2>Example</h2>\n<p>Instead of dropping a comment straight into the HTML, you encode it (<code>&lt;</code> becomes <code>&amp;lt;</code>), so a posted <code>&lt;script&gt;</code> shows up as text. Then you add a header like <code>Content-Security-Policy: default-src &#x27;self&#x27;</code> so inline and third party scripts are blocked by default.</p>\n<h2>Debrief</h2>\n<p>Encode for the context you&#x27;re inserting into: HTML body, HTML attribute, and JavaScript each need different encoding. CSP is defense in depth, not a replacement for encoding.</p>\n<h2>Lab</h2>\n<p>You&#x27;re given the vulnerable comment template from the XSS lesson. Add proper output encoding and set a CSP header on the app. Hidden tests rerun the XSS payload and confirm it no longer executes, while normal comments still display correctly.</p>","short_html":"<h2>Concept</h2>\n<p>The main defense against XSS is output encoding: when you put user data into a page, convert characters like <code>&lt;</code> and <code>&gt;</code> into harmless entities so the browser displays them instead of running them. A Content Security Policy is a second layer that tells the browser which scripts are allowed to run at all, so a missed spot is less likely to be exploitable.</p>\n<h2>Debrief</h2>\n<p>Encode for the context you&#x27;re inserting into: HTML body, HTML attribute, and JavaScript each need different encoding. CSP is defense in depth, not a replacement for encoding.</p>","quiz":[{"q":"The main defense against XSS is:","options":["Validating input length","Encoding output for the context it's placed in","Blocking the word script","Using HTTPS"]},{"q":"What does a Content Security Policy add on top of encoding?","options":["It encrypts the page","It blocks inline and untrusted scripts as a backup layer","It validates form input","It replaces the need for sessions"]}],"lab":{"type":"code","starter_file":"comments.py","objective":"Encode comment output and add a CSP header","tests":["xss_blocked: the stored payload no longer runs","comments_render: normal text still displays"],"needs_flag":false,"needs_code":true}},"parameterized-queries":{"id":"parameterized-queries","title":"Parameterized Queries","unit":2,"branch":"web","difficulty":2,"prereqs":["sqli"],"lab_type":"code","has_quiz":true,"body_html":"<h1>Parameterized Queries</h1>\n<h2>Concept</h2>\n<p>The real fix isn&#x27;t scrubbing bad characters; it&#x27;s never mixing data into the query text. A parameterized query sends the SQL and the data separately: the database compiles the query first, then drops the data into fixed slots, so input can never be read as SQL.</p>\n<h2>Example</h2>\n<p>Vulnerable:</p>\n<pre><code>query = &quot;SELECT * FROM users WHERE username = &#x27;&quot; + user + &quot;&#x27;&quot;\ndb.execute(query)</code></pre>\n<p>Safe:</p>\n<pre><code>query = &quot;SELECT * FROM users WHERE username = ?&quot;\ndb.execute(query, (user,))</code></pre>\n<p>Now a tautology is just a username to search for. No one is named that, so the login correctly fails.</p>\n<h2>Debrief</h2>\n<p>Placeholders bind values, not identifiers. If you ever build a table or column name from user input, you&#x27;re injectable again; validate those against a fixed allowlist.</p>\n<h2>Lab</h2>\n<p>The lab ships two versions of <code>login.py</code>: the vulnerable starter and a fix that uses a placeholder. Run the hidden tests on each. On the starter the exploit succeeds and a test fails; on the fix the exploit fails and a valid login still succeeds. All checks passing on the fix completes the lab.</p>","short_html":"<h2>Concept</h2>\n<p>The real fix isn&#x27;t scrubbing bad characters; it&#x27;s never mixing data into the query text. A parameterized query sends the SQL and the data separately: the database compiles the query first, then drops the data into fixed slots, so input can never be read as SQL.</p>\n<h2>Debrief</h2>\n<p>Placeholders bind values, not identifiers. If you ever build a table or column name from user input, you&#x27;re injectable again; validate those against a fixed allowlist.</p>","quiz":[{"q":"Why does a parameterized query stop SQLi where escaping often fails?","options":["It encrypts the input","The input is never parsed as part of the SQL statement","It blocks the word SELECT","It runs the query as a read only user"]},{"q":"Which is still unsafe even with a parameterized library?","options":["Building a table or column name from user input by string concat","Passing a user's name as a bound parameter","Passing a number as a bound parameter","Using a placeholder for a search term"]}],"lab":{"type":"code","starter_file":"login.py","objective":"Rewrite the login query to use parameters","tests":["exploit_blocked: ' OR '1'='1 fails","normal_login_works: valid creds still succeed"],"needs_flag":false,"needs_code":true}},"path-traversal-uploads":{"id":"path-traversal-uploads","title":"Path Traversal & File Uploads","unit":5,"branch":"web","difficulty":3,"prereqs":["untrusted-input","command-injection"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Path Traversal &amp; File Uploads</h1>\n<h2>Concept</h2>\n<p>Two related ways to abuse how a server handles files. Path traversal is tricking a file endpoint into reading files outside its intended folder. Insecure file upload is getting the server to accept and later run a file it shouldn&#x27;t. Both come from trusting a filename or path from the client.</p>\n<h2>Example</h2>\n<p>A download link uses <code>?file=report.pdf</code>. If the server just joins that onto a base folder, a <code>../</code>-laden value climbs out of the folder and returns a system file. On the upload side, a form that only checks for a <code>.jpg</code> extension can be fed a file that&#x27;s named <code>.jpg</code> but contains server code, which may run if it lands somewhere executable.</p>\n<h2>Debrief</h2>\n<p>For downloads, never build a path from raw input: map an ID to a known filename, or strip and validate the path against a fixed directory. For uploads, verify real content type, rename files, and store them where they can&#x27;t be executed.</p>\n<h2>Lab</h2>\n<p>Two parts. Read <code>flag.txt</code> through a <code>?file=</code> endpoint using <code>../</code> traversal, then bypass a naive extension filter to upload a file that reveals a second flag.</p>","short_html":"<h2>Concept</h2>\n<p>Two related ways to abuse how a server handles files. Path traversal is tricking a file endpoint into reading files outside its intended folder. Insecure file upload is getting the server to accept and later run a file it shouldn&#x27;t. Both come from trusting a filename or path from the client.</p>\n<h2>Debrief</h2>\n<p>For downloads, never build a path from raw input: map an ID to a known filename, or strip and validate the path against a fixed directory. For uploads, verify real content type, rename files, and store them where they can&#x27;t be executed.</p>","quiz":[{"q":"A download endpoint uses ?file=report.pdf. What might ?file=../../etc/passwd do?","options":["Return a system file outside the intended folder","Delete the file","Upload a new file","Nothing; .. is always blocked"]},{"q":"An upload form checks only the .jpg extension. Why is that weak?","options":["JPGs cannot be uploaded","A file can be named .jpg but still contain and run server code","The extension changes the file size","It blocks all images"]}],"lab":{"type":"container","image":"cyberforge/path-traversal-uploads","objective":"Read flag.txt via traversal, then bypass the upload filter","needs_flag":true,"needs_code":false}},"session-management":{"id":"session-management","title":"Session Management","unit":3,"branch":"web","difficulty":3,"prereqs":["authentication"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Session Management</h1>\n<h2>Concept</h2>\n<p>After you log in, the session ID is what keeps you logged in. If an attacker can guess, steal, or fix that ID, they become you without ever knowing your password. This is what happens after authentication, so it follows the previous lesson.</p>\n<h2>Example</h2>\n<p>If session IDs are handed out in order (user 41 gets session 1041, user 42 gets 1042), an attacker who logs in as themselves can guess the IDs around theirs and hijack other users&#x27; sessions. Session fixation is the mirror image: the attacker sets a known ID before the victim logs in, then rides it afterward.</p>\n<h2>Debrief</h2>\n<p>Session IDs must be long and random, issued fresh at login (to defeat fixation), sent as <code>HttpOnly</code> <code>Secure</code> cookies, and expired after inactivity.</p>\n<h2>Lab</h2>\n<p>The app issues sequential session IDs. Log in as a normal user, work out the pattern, and predict the admin&#x27;s session ID to take over their session and reach the flag.</p>","short_html":"<h2>Concept</h2>\n<p>After you log in, the session ID is what keeps you logged in. If an attacker can guess, steal, or fix that ID, they become you without ever knowing your password. This is what happens after authentication, so it follows the previous lesson.</p>\n<h2>Debrief</h2>\n<p>Session IDs must be long and random, issued fresh at login (to defeat fixation), sent as <code>HttpOnly</code> <code>Secure</code> cookies, and expired after inactivity.</p>","quiz":[{"q":"The session ID is 'user id + 1000'. Why is that dangerous?","options":["An attacker can guess other users' session IDs","The ID is too short to store","It expires too quickly","It cannot be sent over HTTPS"]},{"q":"What is session fixation?","options":["Reusing one session for every user","Forcing a victim to use a session ID the attacker already knows","A session that never expires","Storing the session in localStorage"]}],"lab":{"type":"container","image":"cyberforge/session-management","objective":"Predict a sequential session ID to hijack the admin session","needs_flag":true,"needs_code":false}},"sql-data-boundaries":{"id":"sql-data-boundaries","title":"SQL & Data Boundaries","unit":2,"branch":"web","difficulty":2,"prereqs":["untrusted-input"],"lab_type":"container","has_quiz":true,"body_html":"<h1>SQL &amp; Data Boundaries</h1>\n<h2>Concept</h2>\n<p>Apps store data in a database and fetch it with SQL queries. A query is code; a username is data. Injection happens at the exact moment an app lets data cross into the code half of the query. Before you can exploit or fix that, you need to see where the boundary is in real code.</p>\n<h2>Example</h2>\n<p>This builds the query by pasting the name into the string:</p>\n<pre><code>query = &quot;SELECT * FROM users WHERE name = &#x27;&quot; + name + &quot;&#x27;&quot;</code></pre>\n<p>The programmer wrote the SELECT and the quotes; the name came from a stranger. The database receives one blob of text and can&#x27;t tell the two apart, so whatever SQL the stranger typed runs as code.</p>\n<h2>Debrief</h2>\n<p>The safe pattern keeps data and query separate using placeholders (Lesson 7). For now, learn to spot concatenation: a <code>+</code> or a formatted string putting input into SQL is the smell.</p>\n<h2>Lab</h2>\n<p>A search page runs in a debug mode that prints the final SQL it built. Type different inputs and watch your text land inside the query string. The flag unlocks once you submit an input that visibly breaks out of the quotes.</p>","short_html":"<h2>Concept</h2>\n<p>Apps store data in a database and fetch it with SQL queries. A query is code; a username is data. Injection happens at the exact moment an app lets data cross into the code half of the query. Before you can exploit or fix that, you need to see where the boundary is in real code.</p>\n<h2>Debrief</h2>\n<p>The safe pattern keeps data and query separate using placeholders (Lesson 7). For now, learn to spot concatenation: a <code>+</code> or a formatted string putting input into SQL is the smell.</p>","quiz":[{"q":"An app runs \"SELECT * FROM users WHERE name = '\" + name + \"'\". The core problem is:","options":["User data is mixed into the query text as code","SELECT * returns too many columns","The query has no LIMIT clause","The table is not indexed"]},{"q":"Which keeps data and query text separate?","options":["Escaping quotes with string replace","A parameterized query with placeholders","Validating input length","Hiding SQL errors from the user"]}],"lab":{"type":"container","image":"cyberforge/sql-boundaries","objective":"Use the debug view to make input break out of the query string","needs_flag":true,"needs_code":false}},"sqli":{"id":"sqli","title":"SQL Injection","unit":2,"branch":"web","difficulty":2,"prereqs":["sql-data-boundaries"],"lab_type":"container","has_quiz":true,"body_html":"<h1>SQL Injection</h1>\n<h2>Concept</h2>\n<p>When input crosses into the query, an attacker can rewrite what the query does: log in without a password, read tables they shouldn&#x27;t, or dump the whole database. SQLi is exploited first here so the fix in the next lesson has meaning.</p>\n<h2>Example</h2>\n<p>A login runs <code>WHERE username = &#x27;$u&#x27; AND password = &#x27;$p&#x27;</code>. Sending a tautology as the username makes the condition always true, so the database returns the first user (often admin) and the attacker is in, no password needed.</p>\n<h2>Real world breach</h2>\n<p>Heartland Payment Systems, 2008: a SQL injection led to one of the largest card data breaches of its era, over 100 million transactions exposed.</p>\n<h2>Debrief</h2>\n<p>The fix is parameterized queries (next lesson), not filtering characters. Blocklists for quotes and keywords are routinely bypassed.</p>\n<h2>Lab</h2>\n<p>Two parts. First, bypass the login. Second, use a <code>UNION SELECT</code> to read a hidden secrets table that has no page of its own. The flag is a row in that table.</p>","short_html":"<h2>Concept</h2>\n<p>When input crosses into the query, an attacker can rewrite what the query does: log in without a password, read tables they shouldn&#x27;t, or dump the whole database. SQLi is exploited first here so the fix in the next lesson has meaning.</p>\n<h2>Debrief</h2>\n<p>The fix is parameterized queries (next lesson), not filtering characters. Blocklists for quotes and keywords are routinely bypassed.</p>","quiz":[{"q":"What does ' OR '1'='1 in the username field do to WHERE username='$u' AND password='$p'?","options":["Deletes the users table","Makes the WHERE always true, logging in without a password","Crashes the database","Changes the admin password"]},{"q":"Which behaviour hints the backend is vulnerable to SQLi?","options":["It is slow with long input","A single quote produces a database error","It returns no results for emojis","It lowercases the query"]}],"lab":{"type":"container","image":"cyberforge/sqli-login","objective":"Bypass the login, then UNION dump the hidden secrets table","needs_flag":true,"needs_code":false}},"ssrf":{"id":"ssrf","title":"Server Side Request Forgery","unit":5,"branch":"web","difficulty":4,"prereqs":["http-basics","untrusted-input"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Server Side Request Forgery</h1>\n<h2>Concept</h2>\n<p>SSRF is making the server fetch a URL the attacker chooses. Many apps fetch URLs on your behalf (link previews, webhooks, image fetchers). If the app will fetch any URL, an attacker can point it at internal addresses the attacker can&#x27;t reach directly, like other services on the private network or a cloud metadata endpoint.</p>\n<h2>Example</h2>\n<p>A &quot;preview this link&quot; feature fetches whatever URL you paste and shows the result. Paste a link local metadata address or an internal admin URL and the server fetches internal resources and hands you the response, even though those addresses are firewalled off from the outside.</p>\n<h2>Debrief</h2>\n<p>Don&#x27;t let users drive server side fetches freely. Use an allowlist of permitted destinations, block requests to internal and link local IP ranges, and don&#x27;t reflect raw responses back. Blocklisting strings like <code>localhost</code> is weak because of IP and encoding tricks.</p>\n<h2>Lab</h2>\n<p>The container runs the public app plus an internal only service on an isolated Docker network. Use the link preview feature to reach the internal service and pull back a flag that the internet facing side can&#x27;t see directly.</p>","short_html":"<h2>Concept</h2>\n<p>SSRF is making the server fetch a URL the attacker chooses. Many apps fetch URLs on your behalf (link previews, webhooks, image fetchers). If the app will fetch any URL, an attacker can point it at internal addresses the attacker can&#x27;t reach directly, like other services on the private network or a cloud metadata endpoint.</p>\n<h2>Debrief</h2>\n<p>Don&#x27;t let users drive server side fetches freely. Use an allowlist of permitted destinations, block requests to internal and link local IP ranges, and don&#x27;t reflect raw responses back. Blocklisting strings like <code>localhost</code> is weak because of IP and encoding tricks.</p>","quiz":[{"q":"A 'preview this link' feature fetches any URL the user gives. The SSRF risk is:","options":["The server can be made to request internal only addresses","The user's browser is attacked","The link is stored unencrypted","The preview image is too large"]},{"q":"Which is the strongest SSRF mitigation?","options":["Blocking URLs that contain the word localhost","Allowing only an approved list of destinations","Lowercasing the URL","Limiting the response size"]}],"lab":{"type":"container","image":"cyberforge/ssrf","objective":"Use link preview to fetch a flag from the internal only service","needs_flag":true,"needs_code":false}},"untrusted-input":{"id":"untrusted-input","title":"Untrusted Input","unit":1,"branch":"web","difficulty":1,"prereqs":["http-basics"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Untrusted Input</h1>\n<h2>Concept</h2>\n<p>The single idea behind this whole module: the app trusted something the user sent when it shouldn&#x27;t have. Input isn&#x27;t just text boxes. It&#x27;s URL parameters, headers, cookies, hidden fields, and the body, all chosen by the client. The trust boundary is client versus server, not visible versus hidden.</p>\n<h2>Example</h2>\n<p>A checkout form carries the price in a hidden field: <code>&lt;input type=&quot;hidden&quot; name=&quot;price&quot; value=&quot;49.99&quot;&gt;</code>. It&#x27;s hidden on the page but fully editable in the request. Change it to <code>0.01</code> and, if the server bills what it was sent, the order goes through at a penny.</p>\n<h2>Debrief</h2>\n<p>The server must derive again anything that matters from trusted data it controls. For a price, look it up from the product ID server side; never accept a price the client stated.</p>\n<h2>Lab</h2>\n<p>Edit the hidden price field to buy an item for $0.01. The flag appears on the order confirmation, driving home that hidden is not the same as secure.</p>","short_html":"<h2>Concept</h2>\n<p>The single idea behind this whole module: the app trusted something the user sent when it shouldn&#x27;t have. Input isn&#x27;t just text boxes. It&#x27;s URL parameters, headers, cookies, hidden fields, and the body, all chosen by the client. The trust boundary is client versus server, not visible versus hidden.</p>\n<h2>Debrief</h2>\n<p>The server must derive again anything that matters from trusted data it controls. For a price, look it up from the product ID server side; never accept a price the client stated.</p>","quiz":[{"q":"Which data should a server treat as untrusted?","options":["Only URL parameters","Form fields and URL parameters, but not headers","Anything from the client: headers, cookies, and hidden fields too","Only data from logged out users"]},{"q":"A hidden field holds a price. The right fix is to:","options":["Mark the field readonly","Base64-encode the price","Look up the price on the server from the product ID","Validate the price in JavaScript"]}],"lab":{"type":"container","image":"cyberforge/untrusted-input","objective":"Edit the hidden price field to check out for $0.01","needs_flag":true,"needs_code":false}},"web-foundations":{"id":"web-foundations","title":"Web Foundations","unit":1,"branch":"web","difficulty":1,"prereqs":[],"lab_type":"container","has_quiz":true,"body_html":"<h1>Web Foundations</h1>\n<h2>Concept</h2>\n<p>A web app is a conversation between a client (the browser) and a server. You type a URL, the browser finds the server&#x27;s address through DNS, opens a connection, and asks for a page; the server answers. Knowing which side does what is the whole game in security: the client is fully under the user&#x27;s control, and the server is the only place a decision can be trusted to be enforced. Everything in this module comes back to that line.</p>\n<h2>Example</h2>\n<p>Visiting <code>https://shop.example.com/cart</code> runs four steps in order: DNS turns the name into an IP address, TLS sets up an encrypted channel, the browser sends <code>GET /cart</code>, and the server replies with HTML. The page you see is only the server&#x27;s answer; the request that asked for it was built entirely on your machine, where anyone can change it.</p>\n<h2>Debrief</h2>\n<p>Because the client controls the request, never trust it to enforce rules. Hiding a button or checking a value in JavaScript stops an honest user, not an attacker. Real checks live on the server.</p>\n<h2>Lab</h2>\n<p>The container serves a small site. Find two flags: one hidden in an HTML comment in the page source, and one in <code>robots.txt</code>. It teaches you to look past the rendered page at what the server actually sent.</p>","short_html":"<h2>Concept</h2>\n<p>A web app is a conversation between a client (the browser) and a server. You type a URL, the browser finds the server&#x27;s address through DNS, opens a connection, and asks for a page; the server answers. Knowing which side does what is the whole game in security: the client is fully under the user&#x27;s control, and the server is the only place a decision can be trusted to be enforced. Everything in this module comes back to that line.</p>\n<h2>Debrief</h2>\n<p>Because the client controls the request, never trust it to enforce rules. Hiding a button or checking a value in JavaScript stops an honest user, not an attacker. Real checks live on the server.</p>","quiz":[{"q":"When you open a URL, what has to happen before any HTTP request is sent?","options":["The server sets a session cookie","DNS resolves the hostname to an IP address","The page's JavaScript runs","The browser renders the HTML"]},{"q":"Which side of the client/server split can a security decision be trusted on?","options":["The client, because the user is signed in","The server, because the client is fully user controlled","Either, as long as HTTPS is used","The client, if the check is in JavaScript"]}],"lab":{"type":"container","image":"cyberforge/web-foundations","objective":"Find the flag in the page source and in robots.txt","needs_flag":true,"needs_code":false}},"xss":{"id":"xss","title":"Cross Site Scripting","unit":4,"branch":"web","difficulty":3,"prereqs":["cookies-sop","untrusted-input"],"lab_type":"container","has_quiz":true,"body_html":"<h1>Cross Site Scripting</h1>\n<h2>Concept</h2>\n<p>XSS flips the target: instead of attacking the server, you get your script to run in another user&#x27;s browser. If a site shows user supplied content without neutralizing it, an attacker&#x27;s script runs with that victim&#x27;s session, doing whatever the victim could do.</p>\n<h2>Example</h2>\n<p>A comment box saves whatever you type and shows it to everyone. Post a script tag that reads <code>document.cookie</code> and sends it to an attacker URL, and it runs in every visitor&#x27;s browser. That&#x27;s stored XSS; the reflected kind bounces the script off a single response, usually via a crafted link.</p>\n<h2>Real world breach</h2>\n<p>The 2005 Samy worm used stored XSS on MySpace to add over a million friends in a day, a classic demonstration of how fast stored XSS spreads.</p>\n<h2>Debrief</h2>\n<p>The fix is in the next lesson: encode output so markup is shown, not run. Note that <code>HttpOnly</code> stops the script from reading the cookie, but the script can still act as the user, so encoding is still required.</p>\n<h2>Lab</h2>\n<p>The container runs the app plus a simulated &quot;admin bot&quot; (a headless browser) that visits the comments page every few seconds. Post a stored XSS payload that exfiltrates the bot&#x27;s cookie to a capture endpoint; that cookie is the flag.</p>","short_html":"<h2>Concept</h2>\n<p>XSS flips the target: instead of attacking the server, you get your script to run in another user&#x27;s browser. If a site shows user supplied content without neutralizing it, an attacker&#x27;s script runs with that victim&#x27;s session, doing whatever the victim could do.</p>\n<h2>Debrief</h2>\n<p>The fix is in the next lesson: encode output so markup is shown, not run. Note that <code>HttpOnly</code> stops the script from reading the cookie, but the script can still act as the user, so encoding is still required.</p>","quiz":[{"q":"A comment field stores input and shows it to all visitors unescaped. Posting a <script> tag is:","options":["Reflected XSS","Stored XSS","CSRF","SQL injection"]},{"q":"With HttpOnly set on the session cookie, which XSS impact is still possible?","options":["Reading the cookie via document.cookie","Making requests as the victim from their browser","Decrypting the session","None; HttpOnly stops all XSS"]}],"lab":{"type":"container","image":"cyberforge/xss","objective":"Post a stored XSS comment that steals the admin bot's cookie","needs_flag":true,"needs_code":false}}},"graph":{"topo_order":["web-foundations","http-basics","cookies-sop","untrusted-input","authentication","session-management","access-control","csrf","xss","capstone-chain","sql-data-boundaries","sqli","command-injection","output-encoding-csp","parameterized-queries","path-traversal-uploads","ssrf"],"units":{"1":["web-foundations","http-basics","cookies-sop","untrusted-input"],"2":["sql-data-boundaries","sqli","command-injection","parameterized-queries"],"3":["authentication","session-management","access-control"],"4":["csrf","xss","output-encoding-csp"],"5":["path-traversal-uploads","ssrf"],"6":["capstone-chain"]},"unit_numbers":[1,2,3,4,5,6]},"placement":{"id":"web-security-placement","title":"Web Security Placement","description":"Two scenario questions per lesson. The quiz runs unit by unit and places each lesson as Mastered, Review, or Learn.","per_lesson":2,"questions":[{"id":"p01","lesson":"web-foundations","unit":1,"q":"A user types https://shop.example.com/cart. What happens first?","options":["The browser sends GET /cart","The browser looks up the IP for shop.example.com","The server sends a session cookie","The TLS handshake completes"]},{"id":"p02","lesson":"web-foundations","unit":1,"q":"Which check must run on the server, not in browser JavaScript?","options":["A password strength meter","Whether the user is allowed to delete an order","Hiding the admin menu from regular users","Email format validation before submit"]},{"id":"p03","lesson":"http-basics","unit":1,"q":"The server replies 302 with Location: /dashboard and Set-Cookie: session=abc. What does the browser do?","options":["Shows an error, since 302 isn't 200","Stores the cookie, then sends GET /dashboard with it","Resends the login POST to /dashboard","Ignores the cookie until a refresh"]},{"id":"p04","lesson":"http-basics","unit":1,"q":"Using an intercepting proxy, which parts of a request can an attacker change?","options":["Only form fields","Form fields and URL parameters, but not headers","Everything except cookies","All of it: headers, cookies, hidden fields, and body"]},{"id":"p05","lesson":"cookies-sop","unit":1,"q":"Script on https://evil.com runs fetch('https://bank.com/balance'). By default:","options":["The browser blocks the request from being sent","The request is sent, but evil.com can't read the response","The request is sent and the response is readable","The browser prompts the user"]},{"id":"p06","lesson":"cookies-sop","unit":1,"q":"A session cookie has HttpOnly. What does it stop?","options":["The cookie being sent over plain HTTP","JavaScript reading it via document.cookie","The cookie being sent to other sites","The cookie expiring"]},{"id":"p07","lesson":"untrusted-input","unit":1,"q":"Which data should the server treat as untrusted?","options":["URL parameters only","Form fields and URL parameters, not headers","Anything from the client: headers, cookies, hidden fields","Only data from logged out users"]},{"id":"p08","lesson":"untrusted-input","unit":1,"q":"A form has a hidden field value=\"49.99\" for the price. The right fix is:","options":["Make it readonly","Base64-encode the price","Look up the price on the server from the product ID","Validate the price in JavaScript"]},{"id":"p09","lesson":"sql-data-boundaries","unit":2,"q":"An app runs \"SELECT * FROM users WHERE name = '\" + name + \"'\". The core problem is:","options":["User data is mixed into the query text as code","SELECT * returns too many columns","The query has no LIMIT","The table isn't indexed"]},{"id":"p10","lesson":"sql-data-boundaries","unit":2,"q":"Which keeps data and query separate?","options":["Escaping quotes with string replace","A parameterized query with placeholders","Validating input length","Hiding SQL errors from the user"]},{"id":"p11","lesson":"sqli","unit":2,"q":"A login runs WHERE user='$u' AND pass='$p'. What does ' OR '1'='1 in the username do?","options":["Makes the WHERE always true, logging in without a password","Deletes the users table","Crashes the database","Changes the admin password"]},{"id":"p12","lesson":"sqli","unit":2,"q":"A search box reflects results. Which hints the backend is vulnerable to SQLi?","options":["It's slow with long input","A single quote produces a database error","It returns no results for emojis","It lowercases the query"]},{"id":"p13","lesson":"parameterized-queries","unit":2,"q":"Why does a parameterized query stop SQLi where escaping often fails?","options":["It encrypts the input","The input is never parsed as part of the SQL statement","It blocks the word SELECT","It runs the query as a read only user"]},{"id":"p14","lesson":"parameterized-queries","unit":2,"q":"Which is still unsafe even with a parameterized library?","options":["Building a table or column name from user input by string concat","Passing a user's name as a bound parameter","Passing a number as a bound parameter","Using a placeholder for a search term"]},{"id":"p15","lesson":"command-injection","unit":2,"q":"Code runs os.system(\"ping \" + host). What does host = 8.8.8.8; cat /etc/passwd do?","options":["Runs ping, then runs cat /etc/passwd","Pings an invalid host and errors","Is blocked because it has a space","Pings 8.8.8.8;cat as one host"]},{"id":"p16","lesson":"command-injection","unit":2,"q":"The safest fix for the ping tool is:","options":["Remove semicolons from input","Block the words cat and rm","Pass arguments as a list to the program, with no shell","Run the command as a unprivileged user"]},{"id":"p17","lesson":"authentication","unit":3,"q":"A login says 'user not found' vs 'wrong password.' Why is that a problem?","options":["It lets an attacker confirm which usernames exist","It leaks the password hash","It disables rate limiting","It exposes the session cookie"]},{"id":"p18","lesson":"authentication","unit":3,"q":"Which most directly stops online password brute forcing?","options":["Longer password hashes","HTTPS on the login page","Rate limiting and lockout after failed attempts","A CAPTCHA on the signup page"]},{"id":"p19","lesson":"session-management","unit":3,"q":"After login, the session ID is user id + 1000. Why is that dangerous?","options":["An attacker can guess other users' session IDs","The ID is too short to store","It expires too quickly","It can't be sent over HTTPS"]},{"id":"p20","lesson":"session-management","unit":3,"q":"What is session fixation?","options":["Reusing one session for every user","Forcing a victim to use a session ID the attacker already knows","A session that never expires","Storing the session in localStorage"]},{"id":"p21","lesson":"access-control","unit":3,"q":"A logged in user changes /invoice?id=105 and sees someone else's invoice. The missing control is:","options":["Input validation on the id","A check that the invoice belongs to the current user","HTTPS on the endpoint","Rate limiting"]},{"id":"p22","lesson":"access-control","unit":3,"q":"Why doesn't hiding the admin link in the UI protect /admin?","options":["The user can request the URL directly regardless of the UI","The link is cached by the browser","Search engines index the link","The CSS can be disabled"]},{"id":"p23","lesson":"xss","unit":4,"q":"A comment field stores input and shows it to all visitors unescaped. Posting <script>...</script> is:","options":["Reflected XSS","Stored XSS","CSRF","SQL injection"]},{"id":"p24","lesson":"xss","unit":4,"q":"With HttpOnly on the session cookie, which XSS impact is still possible?","options":["Reading the cookie via document.cookie","Making requests as the victim from their browser","Decrypting the session","Nothing; HttpOnly stops all XSS"]},{"id":"p25","lesson":"output-encoding-csp","unit":4,"q":"The main defense against XSS is:","options":["Validating input length","Encoding output for the context it's placed in (HTML, attribute, JS)","Blocking the word script","Using HTTPS"]},{"id":"p26","lesson":"output-encoding-csp","unit":4,"q":"What does a Content Security Policy add on top of encoding?","options":["It encrypts the page","It blocks inline and untrusted scripts from running as a backup layer","It validates form input","It replaces the need for sessions"]},{"id":"p27","lesson":"csrf","unit":4,"q":"CSRF works because the browser automatically includes what with a cross site request?","options":["The victim's cookies for the target site","The victim's password","The attacker's cookies","The page's CSP header"]},{"id":"p28","lesson":"csrf","unit":4,"q":"Which best stops CSRF?","options":["Checking the user is logged in","Using HTTPS","A per request CSRF token the attacker can't know, or SameSite cookies","Hiding the form's action URL"]},{"id":"p29","lesson":"path-traversal-uploads","unit":5,"q":"A download endpoint uses ?file=report.pdf. What might ?file=../../etc/passwd do?","options":["Return a system file outside the intended folder","Delete the file","Upload a new file","Nothing; .. is always blocked"]},{"id":"p30","lesson":"path-traversal-uploads","unit":5,"q":"An upload form checks the .jpg extension. Why is that weak?","options":["JPGs can't be uploaded","A file can be named .jpg but still contain and run server code","The extension changes the file size","It blocks all images"]},{"id":"p31","lesson":"ssrf","unit":5,"q":"A 'preview this link' feature fetches any URL the user gives. The SSRF risk is:","options":["The server can be made to request internal only addresses","The user's browser is attacked","The link is stored unencrypted","The preview image is too large"]},{"id":"p32","lesson":"ssrf","unit":5,"q":"Which is the strongest SSRF mitigation?","options":["Blocking URLs that contain localhost","Allowing only an approved list of destinations","Lowercasing the URL","Limiting the response size"]}]},"tools":{"categories":["Proxy & interception","HTTP clients","Recon & scanning","Injection","Fuzzing & discovery","Browser & in the browser","Static analysis","Containers & isolation","Practice targets","Utilities"],"tools":[{"id":"burp-suite","name":"Burp Suite Community","category":"Proxy & interception","summary":"Intercepting proxy for watching and editing every request the browser sends.","use":"Put the browser behind Burp, open a lab, and edit cookies, headers and form fields in Repeater before they reach the server.","lessons":["http-basics","cookies-sop","untrusted-input","authentication","session-management","access-control","csrf"],"url":"https://portswigger.net/burp/communitydownload","license":"Free (Community)","platform":["Linux","macOS","Windows"]},{"id":"owasp-zap","name":"OWASP ZAP","category":"Proxy & interception","summary":"Open source proxy and scanner from OWASP; the free alternative to Burp.","use":"Same job as Burp. Its passive scanner also flags missing security headers while you browse a lab.","lessons":["http-basics","output-encoding-csp","csrf"],"url":"https://www.zaproxy.org","license":"Apache-2.0","platform":["Linux","macOS","Windows"]},{"id":"mitmproxy","name":"mitmproxy","category":"Proxy & interception","summary":"Terminal and web based intercepting proxy, scriptable in Python.","use":"Good when you want to log or rewrite traffic from a script instead of clicking through a GUI.","lessons":["http-basics","session-management"],"url":"https://mitmproxy.org","license":"MIT","platform":["Linux","macOS","Windows"]},{"id":"curl","name":"curl","category":"HTTP clients","summary":"Command line HTTP client. The fastest way to send one exact request.","use":"Several labs can't be solved from the address bar; curl -X POST, -H and -b let you drive them precisely.","lessons":["http-basics","untrusted-input","command-injection","authentication","csrf","ssrf"],"url":"https://curl.se","license":"MIT style","platform":["Linux","macOS","Windows"]},{"id":"httpie","name":"HTTPie","category":"HTTP clients","summary":"Friendlier curl with JSON by default and colored output.","use":"Nicer for JSON APIs: `http POST :5000/api/login user=admin`.","lessons":["http-basics","authentication"],"url":"https://httpie.io","license":"BSD-3-Clause","platform":["Linux","macOS","Windows"]},{"id":"rest-client","name":"VS Code REST Client","category":"HTTP clients","summary":"Send requests from a .http file inside VS Code.","use":"Keep each lab's requests in a file next to your notes so you can replay them.","lessons":["http-basics","csrf","ssrf"],"url":"https://marketplace.visualstudio.com/items?itemName=humao.rest-client","license":"MIT","platform":["VS Code"]},{"id":"nmap","name":"Nmap","category":"Recon & scanning","summary":"Port scanner and service fingerprinting.","use":"Map what the lab network exposes; in the SSRF lab, reason about what the internal service would look like to a scanner.","lessons":["web-foundations","ssrf"],"url":"https://nmap.org","license":"Nmap Public Source License","platform":["Linux","macOS","Windows"]},{"id":"nikto","name":"Nikto","category":"Recon & scanning","summary":"Web server scanner for known misconfigurations and dangerous files.","use":"Run it against a lab container to see the kind of noise a real scanner produces.","lessons":["web-foundations","path-traversal-uploads"],"url":"https://cirt.net/Nikto2","license":"GPL-2.0","platform":["Linux","macOS"]},{"id":"wireshark","name":"Wireshark","category":"Recon & scanning","summary":"Packet capture and protocol analysis.","use":"Capture a plain HTTP login to the lab and read the password off the wire; then compare with TLS.","lessons":["web-foundations","cookies-sop"],"url":"https://www.wireshark.org","license":"GPL-2.0","platform":["Linux","macOS","Windows"]},{"id":"sqlmap","name":"sqlmap","category":"Injection","summary":"Automates SQL injection detection and exploitation.","use":"Find the injection in the SQLi lab by hand first, then let sqlmap confirm and dump it so you understand what the tool is doing.","lessons":["sql-data-boundaries","sqli","parameterized-queries"],"url":"https://sqlmap.org","license":"GPL-2.0","platform":["Linux","macOS","Windows"]},{"id":"commix","name":"Commix","category":"Injection","summary":"Automated OS command injection testing.","use":"The command injection lab's `ping` endpoint is the textbook case this tool looks for.","lessons":["command-injection"],"url":"https://github.com/commixproject/commix","license":"GPL-3.0","platform":["Linux","macOS","Windows"]},{"id":"ffuf","name":"ffuf","category":"Fuzzing & discovery","summary":"Fast web fuzzer for directories, parameters and virtual hosts.","use":"Find hidden routes and files in a lab (robots.txt, backups, admin paths) and guess the invoice ids in the access control lab.","lessons":["web-foundations","access-control","path-traversal-uploads"],"url":"https://github.com/ffuf/ffuf","license":"MIT","platform":["Linux","macOS","Windows"]},{"id":"gobuster","name":"Gobuster","category":"Fuzzing & discovery","summary":"Directory, DNS and vhost guessing.","use":"Alternative to ffuf for content discovery; pair it with the SecLists wordlists.","lessons":["web-foundations","path-traversal-uploads"],"url":"https://github.com/OJ/gobuster","license":"Apache-2.0","platform":["Linux","macOS","Windows"]},{"id":"seclists","name":"SecLists","category":"Fuzzing & discovery","summary":"The standard collection of wordlists: usernames, passwords, paths, payloads.","use":"Feed its XSS and SQLi payload lists into a fuzzer, or its password lists into the authentication lab.","lessons":["authentication","xss","sqli"],"url":"https://github.com/danielmiessler/SecLists","license":"MIT","platform":["Any"]},{"id":"devtools","name":"Browser DevTools","category":"Browser & in the browser","summary":"Network, Application (cookies/storage) and Console panels built into Chrome and Firefox.","use":"Read Set-Cookie flags, watch the CSRF form submit, and see exactly where an XSS payload lands in the DOM.","lessons":["cookies-sop","session-management","xss","output-encoding-csp","csrf"],"url":"https://developer.chrome.com/docs/devtools","license":"Built in","platform":["Chrome","Firefox","Edge"]},{"id":"csp-evaluator","name":"CSP Evaluator","category":"Browser & in the browser","summary":"Google's checker for Content-Security-Policy headers.","use":"Paste the policy you wrote for the output encoding lab and see which bypasses it still allows.","lessons":["output-encoding-csp"],"url":"https://csp-evaluator.withgoogle.com","license":"Apache-2.0","platform":["Web"]},{"id":"semgrep","name":"Semgrep","category":"Static analysis","summary":"Pattern based static analysis with rulesets for injection, XSS and more.","use":"Run `semgrep --config p/python` on the lab apps to see how a scanner spots the vulnerable fixtures the hidden tests fail on.","lessons":["parameterized-queries","output-encoding-csp","command-injection"],"url":"https://semgrep.dev","license":"LGPL-2.1","platform":["Linux","macOS","Windows"]},{"id":"bandit","name":"Bandit","category":"Static analysis","summary":"Security linter for Python.","use":"Flags string formatted SQL, subprocess with shell=True and hardcoded secrets in the Flask labs.","lessons":["parameterized-queries","command-injection"],"url":"https://bandit.readthedocs.io","license":"Apache-2.0","platform":["Linux","macOS","Windows"]},{"id":"docker","name":"Docker & Compose","category":"Containers & isolation","summary":"Runs each lab as a disposable container with no host mounts and a hardened network.","use":"`docker compose up -d --build` in labs/web-labs or labs/sqli-login. Read the compose file: it is the isolation policy from requirement R4.","lessons":["web-foundations","sqli","ssrf","capstone-chain"],"url":"https://docs.docker.com","license":"Apache-2.0 (Engine)","platform":["Linux","macOS","Windows"]},{"id":"trivy","name":"Trivy","category":"Containers & isolation","summary":"Scans container images and dependencies for known CVEs and misconfigurations.","use":"Scan the lab images before publishing them; check the Dockerfile for root users and open ports.","lessons":["capstone-chain"],"url":"https://trivy.dev","license":"Apache-2.0","platform":["Linux","macOS","Windows"]},{"id":"juice-shop","name":"OWASP Juice Shop","category":"Practice targets","summary":"Deliberately vulnerable web shop with a scoreboard of 100+ challenges.","use":"Extra practice after a unit: every lesson here has several matching Juice Shop challenges.","lessons":["sqli","xss","access-control","authentication"],"url":"https://owasp.org/www-project-juice-shop/","license":"MIT","platform":["Docker","Node"]},{"id":"webgoat","name":"OWASP WebGoat","category":"Practice targets","summary":"Lesson style vulnerable app used in many university courses.","use":"Closest in spirit to CyberForge; useful to compare how it sequences the same topics.","lessons":["sqli","xss","csrf","path-traversal-uploads"],"url":"https://owasp.org/www-project-webgoat/","license":"GPL-2.0","platform":["Docker","Java"]},{"id":"portswigger-academy","name":"PortSwigger Web Security Academy","category":"Practice targets","summary":"Free hosted labs for every web vulnerability class, with learning paths.","use":"The reference for how a hosted lab platform handles per student instances, which 491B needs to solve.","lessons":["sqli","xss","csrf","ssrf","access-control"],"url":"https://portswigger.net/web-security","license":"Free","platform":["Web"]},{"id":"cyberchef","name":"CyberChef","category":"Utilities","summary":"Encode, decode, hash and transform data in the browser.","use":"URL encode payloads, decode session tokens and base64 cookies, and build encoded XSS strings.","lessons":["cookies-sop","session-management","xss","output-encoding-csp"],"url":"https://gchq.github.io/CyberChef/","license":"Apache-2.0","platform":["Web"]},{"id":"jwt-io","name":"jwt.io","category":"Utilities","summary":"Decode and inspect JSON Web Tokens.","use":"Read the claims in a session token from the labs and see why the signature matters.","lessons":["session-management","authentication"],"url":"https://jwt.io","license":"Free","platform":["Web"]},{"id":"hashcat","name":"hashcat","category":"Utilities","summary":"GPU password hash cracker.","use":"Crack the hashes dumped in the SQLi UNION step to show why unsalted fast hashes fail.","lessons":["sqli","authentication"],"url":"https://hashcat.net","license":"MIT","platform":["Linux","macOS","Windows"]}]},"capstone_id":"capstone-chain","capstone_units":[1,2,3,4],"statuses":{"mastered":"mastered","review":"review","learn":"learn"}}