DEV Community

Cover image for Reflected XSS: How a Simple Input Field Can Execute Code in Your Browser
Jer Catallo
Jer Catallo

Posted on

Reflected XSS: How a Simple Input Field Can Execute Code in Your Browser

Cross-Site Scripting (XSS) is one of the most common web vulnerabilities listed in the OWASP Top 10. In reflected XSS, the server takes user input from a request, places it directly into the HTML response, and sends it back. The browser then renders that response, and if the input contains a script, the browser runs it.

A basic vulnerable pattern looks like this:

// Example: vulnerable PHP - user input is reflected without sanitization
$name = $_GET['name'];
echo "<h2>Hello, " . $name . "</h2>";
Enter fullscreen mode Exit fullscreen mode

When a user sends <script>alert('XSS')</script> as the name value, the server returns:

<h2>Hello, <script>alert('XSS')</script></h2>
Enter fullscreen mode Exit fullscreen mode

The browser parses this as valid HTML and executes the script. This is reflected XSS. The payload is not stored anywhere; it lives in the URL or form input and bounces right back in the response.

The six levels below walk through different injection contexts, each requiring a different approach to trigger the payload.


Ethical Considerations

All testing shown here is done on a controlled, purpose-built lab environment (TryHackMe XSS Examples). Never test XSS payloads on systems you do not own or have written permission to test. Unauthorized testing is illegal and unethical regardless of intent.


Level 1: Direct Reflection - No Filter

The simplest case. The app takes the name from the query string and places it directly inside an <h2> tag with no encoding or filtering.

You can start by checking how the app handles a normal input:

You can see the DOM shows <h2>Hello, Adam</h2>. The input is written as raw text with no escaping applied.

Payload:

<script>alert('THM');</script>
Enter fullscreen mode Exit fullscreen mode

You can see the <script> tag is now nested inside the <h2> element. The page shows "Confirming XSS Payload..." as the script fires. This works because the server writes the input directly into the HTML with no checks.

Remediation: Encode output before writing it into HTML. In PHP, use htmlspecialchars($name, ENT_QUOTES, 'UTF-8'). This converts < and > to &lt; and &gt;, so they display as text and not markup.


Level 2: Breaking Out of an HTML Attribute

Here, the user input is placed inside the value attribute of an <input> tag.

You can see the source reads <h2>Hello, <input value="Adam"></h2>. A plain <script> tag will not work here because it will be trapped inside the attribute value as a string. To escape, you need to close the attribute and the tag first.

Payload:

"><script>alert('THM');</script>
Enter fullscreen mode Exit fullscreen mode

The " closes the value attribute, the > closes the <input> tag, and then the script tag runs.

You can see the <input> element is now broken - <input value> sits empty - and <script>alert('THM');</script> follows right after it. The page reports "XSS Payload Successful."

Remediation: Encode output placed inside HTML attributes. Use htmlspecialchars($value, ENT_QUOTES) to prevent " and > from closing the attribute context.


Level 3: Escaping a Textarea Tag

The input is now reflected inside a <textarea> tag.

You can see the source reads <h2>Hello, <textarea>Adam</textarea></h2>. A <script> tag inside <textarea> will not execute because the browser treats all its content as plain text. You need to close the tag first.

Payload:

</textarea><script>alert('THM');</script>
Enter fullscreen mode Exit fullscreen mode

You can see <textarea></textarea> is now closed, followed by <script>alert('THM');</script>. The browser exits the textarea context and runs the script. Page reports "XSS Payload Successful."

Remediation: Encode special characters on output. The </textarea> sequence should be encoded so it cannot prematurely close the tag.


Level 4: JavaScript String Injection

This level is different. The input is not placed directly in the HTML - it is written into a JavaScript innerHTML assignment.

You can see the source shows document.getElementsByClassName('name')[0].innerHTML='Adam';. The input sits inside a JavaScript string. The approach is to break out of the string, execute a function, and comment out the rest of the original code.

Payload:

';alert('THM');//
Enter fullscreen mode Exit fullscreen mode

The ' closes the string, ; ends the statement, alert('THM') runs, and // comments out the trailing ';.

The DOM shows the modified script block:

document.getElementsByClassName('name')[0].innerHTML='';alert('THM');//';
Enter fullscreen mode Exit fullscreen mode

You can see the alert fires. Page reports "XSS Payload Successful."

Remediation: Never concatenate user input directly into JavaScript strings. Pass values through json_encode() in PHP or JSON.stringify() in Node, or use data attributes and read them with the DOM API.


Level 5: Bypassing a Keyword Filter

Level 5 strips the word script from the input before reflecting it. A standard payload fails here.

Failed attempt:

<script>alert('THM');</script>
Enter fullscreen mode Exit fullscreen mode

You can see the filter strips script from both the opening and closing tags. The DOM renders Hello, <>alert('THM');</> and the page shows "XSS Payload Failed."

You can confirm in the source: <h2>Hello, <>alert('THM');</></h2>. The filter is a simple single-pass string replacement. The bypass is to nest the keyword so that after removal, the word reforms.

Payload:

<sscriptcript>alert('THM');</sscriptcript>
Enter fullscreen mode Exit fullscreen mode

When the filter removes script from sscriptcript, what remains is script. The tags reconstruct themselves.

You can see the DOM now shows <script>alert('THM');</script>. Page reports "XSS Payload Successful."

Remediation: Single string replacement is not a reliable filter. Use a proper HTML parser or a well-tested sanitization library like DOMPurify. Never build your own XSS filter from scratch.


Level 6: Event Handler Injection on an Image Tag

This level accepts a file path and places it into the src attribute of an <img> tag. It also filters < and >, so angle-bracket payloads will not work.

Normal behavior:

You can see the input /images/cat.jpg becomes <img src="/images/cat.jpg">. The DOM shows the cat image loading correctly.

Failed attempt with angle brackets:

"><script>alert('THM');</script>
Enter fullscreen mode Exit fullscreen mode

You can see the filter strips angle brackets and the script keyword. The DOM shows <img src scriptalert('thm'); script"> - completely broken. Page reports "XSS Payload Failed."

The key here is to stay inside the tag. Since you cannot inject new tags, you can inject a new attribute instead. Close the src value with " and add an event handler.

Payload:

/images/cat.jpg" onload="alert('THM');
Enter fullscreen mode Exit fullscreen mode

You can see the DOM now shows:

<img src="/images/cat.jpg" onload="alert('THM');">
Enter fullscreen mode Exit fullscreen mode

When the image loads, the onload handler fires. Page reports "XSS Payload Successful." Flag: THM{XSS_MASTER}.

Remediation: Filtering angle brackets alone is not enough. Encode " inside attribute values too. Use htmlspecialchars($value, ENT_QUOTES) which handles both <> and "'.


Bonus: Polyglot XSS Payload

A polyglot XSS payload is a single string designed to work across multiple injection contexts at the same time - inside attributes, script blocks, HTML tags, and more. It is useful when you are not sure of the exact injection point.

Payload:

jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */onerror=alert('THM') )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNlOAd=alert('THM')//>\x3e
Enter fullscreen mode Exit fullscreen mode

You can see the full polyglot reflected inside <img src="...">. The payload covers: a JavaScript URI via jaVasCript:, an onerror event handler, URL-encoded newlines to break single-line filters, tag-closing sequences for style, title, textarea, and script, and an SVG onload event. The page reports "XSS Payload Successful" and the flag THM{XSS_MASTER} appears again, confirming the payload fired.

Remediation: There is no single payload type to block when it comes to polyglot attacks. The only reliable fix is context-aware output encoding at every point where user input is rendered. Use a strict Content Security Policy (CSP) to limit which scripts can run, and consider a WAF with XSS rule sets as an extra layer. Sanitization libraries like DOMPurify handle the complexity of multiple contexts so you do not have to.


Summary

Reflected XSS does not need a database or persistent storage. The attacker just needs a URL with a crafted payload and a victim who clicks it. Each level here shows a real pattern and how it can be bypassed when output is not handled correctly.

Level Injection Context Bypass Technique
1 Plain HTML inside <h2> Direct <script> tag
2 HTML value attribute Close attribute with ">
3 <textarea> content Close tag with </textarea>
4 JavaScript string via innerHTML Break string with ';
5 HTML output with script keyword filter Nested keyword: <sscriptcript>
6 <img src> attribute with angle bracket filter Attribute injection via onload

The core fix for all of these is the same: encode output for the context it appears in. HTML encode for HTML body, attribute encode for attributes, and JSON encode for JavaScript. A Content Security Policy (CSP) can also reduce the impact by restricting which scripts the browser is allowed to run.

Top comments (0)