l1ackers
Not marked. Sign in and it follows you. Next: 050: A proxy you install on purpose

~/tutorials/049-what-you-were-not-shown.md

049: What you were not shown

Lesson 047 took back the conversation. Lesson 048 drew the fence. This one reads the response body the way an attacker reads it: as a document with more in it than the rendering shows. The page you see is one interpretation of the HTML. The HTML itself is the whole shipment, and it arrives carrying things the browser never paints: comments, fields, constraints, and clues about the machine that built it.

What this lesson is for

Every input validation you can see in a browser is a suggestion. Every constraint that matters is enforced somewhere you cannot see, on the server, and the gap between the two is where half of web application testing lives. This lesson teaches you to read the raw body as a shipment manifest: what was sent, what was rendered, what was politely left invisible, and what the difference tells you about the code behind the page.

What the reader meets

The honest lab box from lesson 033, stood up the same way. Its login page, read as a shipment, carries this:

$ curl -s http://127.0.0.1:4282/login.php | grep -oE '<!--[^>]{15,90}'
<!--<div id="header"
<!-- <img src="dvwa/images/RandomStorm.png" /
<!--<div id="content"
<!--<div id="footer"

Four HTML comments, shipped to every visitor, rendered by nobody. They are leftovers, commented out markup, a vendor logo that was retired. They are harmless here. They are the lesson anyway, because the same mechanism that ships these ships the ones that are not harmless: the comment above a disabled admin form that says what the form did, the checkin note with an internal path, the deployment stamp with a date and a name. The browser renders none of it. The body carries all of it. Grep the body and you read what the developer thought they had thrown away.

A fact from the writing of this very site, as a receipt you can check: l1ackers.com strips HTML comments before serving, which is why this site's own pages show zero comments in view source. That silence is a rendering decision, made on purpose. Most sites make no such decision, and their shipped bodies say more than their pages do.

The instrument

Three reads, cheapest first.

The comment census, shown above. One grep against the raw body. Run it on every target before you have a plan and you will regularly know the page's history before you know its present.

The form census. Every input the page ships, rendered or not:

$ curl -s http://127.0.0.1:4282/vulnerabilities/sqli/ | grep -oE '<input[^>]{10,90}>'
<input type="text" size="15" name="id">
<input type="submit" name="Submit" value="Submit">

Two inputs. Now the reading discipline: type="text" and a name. Anything else in the tag is a promise the browser keeps and the server may not. A maxlength on a text input stops a human typing. It does not stop a POST, because the POST is built by whoever sends it. A type="hidden" input ships a value the page never renders but the form submits, and those values are sometimes the interesting half of the page: session context, record identifiers, prices, flags. A disabled field is not even submitted by a well behaved browser, which makes it the single most tempting thing on the page to submit on purpose, because if the server reads it anyway, the disabling was theater.

The constraint check. This is the read that changes how you see every form, and this site demonstrates it on itself, live, right now. The sign up form ships this input:

<input name="password" type="password" autocomplete="new-password" required>

No minlength, even though the site enforces ten characters. The required attribute will stop a browser from submitting an empty field, a courtesy to the user. Send the same registration without a browser and the courtesy is gone, and what answers is the real rule, the one that was always doing the work:

$ curl -s -X POST https://l1ackers.com/api/register \
    -H "Content-Type: application/json" \
    -d '{"handle":"len9test","password":"Fall2026!","email":"[email protected]"}'
{"error":"Passwords need at least 10 characters."}

That is a 400 from the server, receipted while writing this lesson, with a nine character password that no browser-side attribute ever promised to stop. The right lesson from this receipt is the one this site wants you to take: the enforcement that matters was on the server all along, where it belongs. The testing instinct it trains is to find the places where it is not. For every constraint you can see in a form, the question to write down is: who enforces this, the browser or the code. You cannot see the code yet. You can see whether the constraint survives a POST that ignores it, and that experiment is one curl line.

Try it

  1. Stand up the lab box from lesson 033 and run the comment census and the form census on its login page and two of its vulnerability pages. Write down every hidden input and every commented block, and guess in one sentence each what it was for. You are learning to read a page's history, which is a form of enumeration.
  2. On this site, read the sign up form's raw HTML and list every attribute that is a browser promise rather than a server rule. Then test one of them the way this lesson tested the password length: send the POST that ignores it, read the answer, and classify the site's enforcement as real or theater. This site's is real. Not every site you test will be, and the ones that are not are where findings live.
  3. The verb-side seam. Request the same lab page with GET, then ask for it with a different Accept header, then compare. Most pages answer identically, and knowing that is a baseline. The pages that answer differently are configured on purpose, and configuration is information.
Reveal the answer

The full checklist, the not-shown census, in the order to run it against any in-scope page:

$ curl -s URL | grep -oE '<!--[^>]{15,}'        # comments, the thrown-away
$ curl -s URL | grep -oE '<input[^>]+>'          # every field, hidden included
$ curl -s URL | grep -oE '<form[^>]+>'           # where each field submits
$ curl -s URL | grep -oiE '(maxlength|minlength|pattern|required|disabled|readonly)[^ >]*'   # the browser promises
$ curl -s URL | grep -oiE 'token|csrf|debug|test|admin'        # the vocabulary of the interesting

Five lines, run in seconds, and together they answer: what was hidden, what was promised, where it goes, and which words appear that the rendered page never says. The last line is the one that finds things. A page that renders politely and ships the word debug in a comment or a hidden field is a page that has a debug mode, and now you know to look for where it is turned on.

Then the experiment each promising attribute earns, one per constraint: send the POST that ignores it. The two possible answers are the whole taxonomy. The server enforces it, and the constraint was a user courtesy wearing a security costume. The server does not, and you have found the gap between what the browser promises and what the code delivers, which is the definition of a client side enforcement failure and the start of a finding in the shape of lesson 044.

What this lesson does not claim

Reading the body does not read the application. Everything here is one shipment, one moment, and the page may build differently for different sessions, different referrers, different cookies. The census is a photograph, not a film. And nothing in this lesson bypasses anything: the POST that ignores a browser promise is a question, not an exploit, and the answer is a fact about where enforcement lives. What you do with that fact is bounded by the fence of lesson 048, which has not moved.

Not marked. Sign in and it follows you. Next: 050: A proxy you install on purpose
l1ackers · shell this is a way in, not a requirement
$