loading...

JavaScript: Watch out for unwanted hoisting!

antogarand profile image Antony Garand ・2 min read

Challenge

Let me start of this post with a small challenge.

Replace // Your code here with actual code, and print Flag!

function generateSecret() {
  return Date.now() + Math.random() * 10000;
}

const mySecretKey = generateSecret();

// Your code here

if (mySecretKey === 42) {
    console.log('Flag!');
} else {
    console.log('Bad secret!');
}

Writeup

In order to print the Flag, we need to understand how function hoisting works.

myFunction();

function myFunction() {
    console.log('My function was called!');
}

This snippet is valid and will correctly print My function was called!, even though this function is declared after it has been called.

This works thanks to Hoisting.

Here is a quick definition from MDN:

Conceptually, for example, a strict definition of hoisting suggests that variable and function declarations are physically moved to the top of your code, but this is not in fact what happens. Instead, the variable and function declarations are put into memory during the compile phase, but stay exactly where you typed them in your coding.

This means that the previous code can be understood as:

function myFunction() {
    console.log('My function was called!');
}

myFunction();

The function declarations and definitions are moved before the actual code happens, which lets us use functions before they are declared.
But what happens if we declare the same function twice?

function myFunction() {
    console.log('My function was called!');
}

myFunction();

function myFunction() {
    console.log('My *evil* function was called!');
}

Spoiler alert: The evil function is called!

Once hoisted, the previous code can be understood as:

function myFunction() {
    console.log('My function was called!');
}
function myFunction() {
    console.log('My *evil* function was called!');
}

myFunction();

As the last declaration of myFunction is the evil one, all of the calls to myFunction will be to the evil function!

Solution

In order to solve the challenge, we therefore only need to redeclare the generateSecret function.

function generateSecret() {
  return Date.now() + Math.random() * 10000;
}

const mySecretKey = generateSecret();

// Your code here
function generateSecret() {
  return 42;
}

if (mySecretKey === 42) {
    console.log('Flag!');
} else {
    console.log('Bad secret!');
}

References

MDN: Hoisting

MDN: Function

Medium: Hoist your knowledge of JavaScript hoisting

Original on GitLab

Discussion

pic
Editor guide
Collapse
bgadrian profile image
Adrian B.G.
  1. I couldn't understand what is the challenge (to force the value to be 42), maybe is just me

  2. Shouldn't be IDE's and linters handle this mistakes for us? (not double declare something). I guess that is the purpose, to raise awareness of this possible problem.

  3. a better challenge would be that wouldn't require to solve it with a bad practice (redeclare/overwrite a definition).

Collapse
antogarand profile image
Antony Garand Author

Hey there, thanks for the feedback!

This is not a "best practice" use case, but one feature of ecmascript that should be known by developers.

The challenge itself is an introduction to a given scenario where you can control only part of the website, such as in a Reflected XSS, yet need to change the behavior of a constant.
In another language, I would expect the secret variable to be safe and tamper-proof, yet it is not thanks to Function Hoisting.

Of course I wouldn't recommend anyone to use this knowledge in clean code, but I definitely can see this being part of a CTF challenge or causing innatention bugs.

Collapse
kepta profile image
Kushan Joshi

Good to know !
But I am afraid why would anyone want to declare function twice to override it. To me code smells 🤷🏽‍♀️

Collapse
dance2die profile image
Sung M. Kim

I've run into the situation multiple times with old codes.

E.g.)
Some JS files I dealt with are about 1k+ lines long and many people added the same function named get*ById etc.

Collapse
kepta profile image
Kushan Joshi

Oh man thats scary, If someone adds another function getById later on. Any code which was using old getById would start using new getById.

To me this sounds like a bug rather than intentional old way of doing things. What do you think it was ?

Thread Thread
dance2die profile image
Sung M. Kim

My guess is that, some devs used function expressions (e.g. var getById = function()...) instead of function declarations (function getById()...)

Function declarations are hoisted with body but function expressions are not, causing people who don't know how hoisting work declare same code over and over sometimes.

Collapse
dance2die profile image
Sung M. Kim

Thanks Antony for the article 🙇

Knowing this, I could debug better with legacy codes 👍