June 22, 2026
Breaking Out of a JavaScript String: My Ninth XSS Lab on PortSwigger
I’ve been working through the PortSwigger Web Security Academy XSS labs one by one, and this ninth lab moved the injection point somewhere…

By Diya
4 min read
I've been working through the PortSwigger Web Security Academy XSS labs one by one, and this ninth lab moved the injection point somewhere new: instead of HTML body content or an HTML attribute, the reflection happens inside a JavaScript string literal. Here's how it went.
Category: Reflected Cross-Site Scripting (XSS)
Difficulty: Apprentice
Lab Link: Reflected XSS into a JavaScript string with angle brackets HTML encoded
Objective
Perform a cross-site scripting attack that breaks out of the JavaScript string and calls the alert function.
Vulnerability Overview
This lab demonstrates a reflected XSS vulnerability where the search query is reflected directly inside a JavaScript string literal, as part of an inline script that tracks search terms. Angle brackets are HTML-encoded, so injecting a new HTML tag such as <script> is not possible. However, single quotes are not encoded, which means the search term can break out of the existing string and inject arbitrary JavaScript that executes as soon as the script runs, no additional user interaction required.
Steps to Reproduce
- Opening the lab reveals a blog home page with a search box.
- Submitting a test input (
abc123) and inspecting the page via DevTools reveals where the reflection happens.
Inspecting the page source shows an inline <script> block containing the following code:
var searchTerms = 'abc123';
document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');var searchTerms = 'abc123';
document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
The search term is assigned directly into a JavaScript string literal (var searchTerms = 'abc123';) with no encoding of single quotes. This is the injection point.
- Since single quotes are not encoded, the payload works by closing the existing string, injecting a JavaScript expression, and reopening a new string to keep the surrounding syntax valid:
'-alert(1)-''-alert(1)-'This transforms the line into:
var searchTerms = ''-alert(1)-'';var searchTerms = ''-alert(1)-'';Which JavaScript parses as a subtraction expression: an empty string minus alert(1) minus another empty string. Since alert(1) is evaluated as part of the expression, it executes immediately, and the syntax remains valid so the rest of the script continues to run without errors.
Entering the payload into the search box:
- Submitting the payload causes the alert to fire automatically as soon as the page loads, since the injected code runs as part of the page's normal script execution, with no click or hover needed.
Inspecting the DOM afterward confirms the injected script line:
var searchTerms = ''-alert(1)-'';var searchTerms = ''-alert(1)-'';
The lab is marked as solved as soon as the alert fires.
Technical Evidence
The root cause is visible by comparing the reflection before and after the payload. With a normal input like abc123, the script reads:
var searchTerms = 'abc123';var searchTerms = 'abc123';With the payload '-alert(1)-' submitted, the same line becomes:
var searchTerms = ''-alert(1)-'';var searchTerms = ''-alert(1)-'';The search term is concatenated directly into the JavaScript source as a string literal without escaping single quotes. Because the surrounding code only HTML-encodes angle brackets and not the characters meaningful inside a JavaScript string context, the single quote in the payload terminates the original string early, allowing arbitrary code to be inserted into the script's execution flow.
Root Cause
The application reflects user input into a JavaScript string context but applies HTML encoding instead of JavaScript string encoding. HTML encoding neutralizes characters like < and >, which matter in an HTML body or attribute context, but it does nothing to protect a JavaScript string literal, where the dangerous character is the single quote (or double quote, depending on how the string is delimited). Since the encoding applied does not match the context the data is reflected into, the input can break out of the string and inject executable code.
Impact
An attacker could craft a malicious URL containing the payload in the search parameter and send it to a victim. Because the injected code executes automatically when the page loads, no further interaction (like a click or hover) is required from the victim. Potential consequences include:
- Session hijacking via cookie theft
- Performing actions on behalf of the victim within the application
- Redirecting the victim to a phishing or malware-distributing site
- Modifying page content as seen by the victim
The lack of any required interaction makes this variant particularly dangerous, since simply visiting the crafted link is enough to trigger the exploit.
Remediation
To prevent this type of vulnerability, the application should:
- Apply context-aware output encoding. When reflecting user input inside a JavaScript string literal, use JavaScript string escaping (e.g., escaping single quotes, double quotes, and backslashes) rather than HTML encoding.
- Avoid embedding user-controlled data directly into inline scripts. Where possible, pass data to JavaScript via safe channels, such as
data-*attributes read bydataset, or JSON embedded in a<script type="application/json">block and parsed withJSON.parse(). - Use a templating or serialization library that understands JavaScript string context and escapes accordingly, instead of manually concatenating strings into script source.
- Implement a strict Content Security Policy (CSP) to limit the impact of any successful injection, particularly by disallowing inline scripts and requiring nonces or hashes for legitimate inline code.
Conclusion
This lab highlighted something subtle: encoding the right characters for the wrong context is effectively the same as not encoding at all. The angle-bracket encoding here would have stopped an HTML tag injection cold, but it was irrelevant once the injection point turned out to be inside a JavaScript string rather than the HTML body. Nine labs in, and the pattern keeps repeating in different forms: knowing where exactly user input lands in the response matters just as much as knowing that it's reflected at all.