Preface

I find myself muttering “Hmm…I wonder if this will work…” under my breath a lot, usually as an excuse to distract myself from whatever task I’m supposed to be working on. On a good day I’m able to stifle the urge to pull on whichever thread has stolen my attention, but this was not one of those days.

The musing caught me shortly after I finished writing an anonymous function declaration in a Javascript codebase I was working on. It prompted me to slap together a five-line code review challenge that ended up being quite tricky for many folks to solve.

The Challenge

The challenge itself is quite simple - review the code, guess the output, then verify your guess by executing the code.

/*
Simple code review challenge. Guess the output, then run it and see what it actually is.
*/
const AGE = 25
const MAX_AGE = 20

// if the condition fails, run this function
const failFunction = (() => console.log('Old'))

// if the condition succeeds, run this function
const successFunction = (() => console.log('Young'))

// Do a check to see if the age is less than or equal to the max age and run the appropriate function
(AGE <= MAX_AGE) === true ? successFunction() : failFunction()

Pretty straightforward, right? Let’s break it down from the perspective of a normal entry/mid-level Javascript/Typescript developer.

Below, we have two numeric constants being defined and assigned. This seems pretty normal.

const AGE = 25
const MAX_AGE = 20

Next we have two functions being defined for later usage.

// if the condition fails, run this function
const failFunction = (() => console.log('Old'))

// if the condition succeeds, run this function
const successFunction = (() => console.log('Young'))

And finally we have a ternary operation that compares AGE and MAX_AGE, checks if truthy, and evaluates the successFunction if true, otherwise it evaluates the failFunction.

// Do a check to see if the age is less than or equal to the max age and run the appropriate function
(AGE <= MAX_AGE) === true ? successFunction() : failFunction()

Since we know the AGE and MAX_AGE values, we can simplify the last line of code like:

(false) === true ? successFunction() : failFunction()

As a developer, I’d probably say something like:

“Well, false is not equal to true, therefore failFunction would run. So the output would be ‘Old’. Final answer.”

This would be a perfectly reasonable guess, and this was the conclusion that the majority of the folks I polled arrived at.

The Actual Results

So when we run the original script above, we can see the following console output:

Young
Old

This is the point where the folks I gave this challenge to started scratching their heads. I was getting questions like:

  • “Is the ternary causing them to both evaluate?”
  • “Is it due to the strict type equality checking?”
  • “Wait..what?”
  • “I hate Javascript.” (Not a question, but worth including because lol)

How It Works

This challenge works really well due to the following:

  1. The Javascript parser is super loose and greedy.
  2. By leaving comments, we can lead the developer on a bit and control the narrative regarding execution flow.
  3. The code itself is incredibly simple, so the developer makes very reasonable assumptions.

Let’s take a look at a stripped down version of the code.

const AGE = 25
const MAX_AGE = 20
const failFunction = (() => console.log('Old'))
const successFunction = (() => console.log('Young'))
(AGE <= MAX_AGE) === true ? successFunction() : failFunction()

The things that should jump out at you as “odd”, but not entirely suspicious:

  • Extra parentheses around the function definitions.
    • In many cases this is fine. const ten = (10); still evaluates to 10. Maintaining the correct order of operations is (in my opinion) a good habit for larger expressions.
  • No semicolons.
    • Javascript doesn’t require you to use semicolons, and in fact, some linter profiles within the JS/TS ecosystem have rules to prevent the developer from using semicolons.
  • Semi-confusing ternary.
    • As mentioned above, managing the order of operations for an expression is important.

Now here’s what is actually happening

const failFunction = (() => {
	 console.log('Old')
})

const successFunction = (() => {
	console.log('Young')
})(AGE <= MAX_AGE)

if (successFunction === true) {
	successFunction()
} else {
	failFunction()
}

Wait, what?

Yep. The additional parentheses around the anonymous function definitions and the lack of semicolons creates a Immediately Invoked Function Expression.

The successFunction anonymous function invokes itself, and assigns undefined to the successFunction variable.

Then the ternary does a comparison evaluation to check if undefined === true, which it isn’t, and evaluates failFunction.

“But you have whitespace and comments between the function and parameter segments!”

Yep! Javascript doesn’t care about trivial things like that if you’re not using semicolons.

// The following will output "2" to the console.
console.log
/* blah blah */
/* blah blah */
(1 + 1)

Is this actually a problem?

Under the right circumstances it could be. Take a look at the following “realistic” snippet:

// index.js
// This is the main entrypoint for the application.
// It detects whether or not we're in a production 
// environment and runs the appropriate function.  

// This is the real function that gets called
const startApp = (() => {
	console.log('The real function was called. Awesome!')
})

// This debug function only exists for dev purposes.
// It never actually gets called, so don't worry about it!
const startAppDebug = (() => {
	console.log('🤠 howdy partner')
	// Database "drop table" migration(s)?
	// Dump database?
	// Write creds to publicly-accessible log file for "debug purposes"?
})

// Check if we're in debug more or not
(process.NODE_ENV !== 'production') ? startAppDebug() : startApp();

Now consider the following:

  • JSLint/ESLint profiles are usually committed to repositories.
    • With a bit of scraping and JSON parsing, one can very easily find repositories which wouldn’t trigger a build failure due to the lack of semicolons.
      • This includes repos without linter configuration at all.
    • One can sort by stars or NPM downloads to find high-impact modules to target.
  • Open source maintainers are (typically) overworked and unappreciated.
    • New PR’s are always welcome, especially from folks who have established themselves as being helpful in the past.

Obviously I’m not saying that this is a huge issue, but given the number of folks I polled, this could be an effective way to slip some sketchy code into a repo or even to a targeted user.

Exfiltrating ~/.ssh, .env, etc could be done quite easily under the right circumstances if the tactic above is implemented in a Node environment. One might attempt to steal cookies, track users, or append remote scripts on the frontend.

Mitigation

This kind of syntax abuse can be mitigated by:

Credits & Shoutouts