DEV Community

Cover image for Penetration and Security in JavaScript
Francesco Di Donato
Francesco Di Donato

Posted on • Updated on

Penetration and Security in JavaScript

Premise

Are you sure you are ensuring your code to be used as intended? Are you preventing it from beeing used in a malicious way?

If what comes your way is putting guards in your functions, this post will open up a world for you as this one was for me. Using checks is not enough.

Index

You will be both wolf and sheep. I created the function below so that it had everything you need to learn attack and related defenses from the techniques:

  1. Probing & Double Getter
  2. Prototype Bribing
  3. Primitive Illusion

The function is Connector, which receives an options configuration object. This must contain a property named address which must be the same as one of those listed in validAddresses, otherwise an exception is thrown.

Once the connection with one of the valid addresses has been established, the instance provides the transfer method to move a certain amount passed as input which must not exceed the value 500.

function Connector(options) {
  const validAddresses = ['partner-account', 'investments', 'mutual']

  if (!options.address || typeof options.address !== 'string') _err1()

  if (!validAddresses.includes(options.address)) _err2(options, validAddresses)

  console.info(`Connection to address [${options.address}] enstablished`)

  return {
    transfer,
  }

  function transfer(amount) {
    if (!amount || amount <= 0) _err3()

    if (amount > 500) _err4()

    console.info(
      `Transfered an amount of [${amount}] to the address [${options.address}]`
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

Do not focus on _err functions. Not important here.

The happy path is the following:

const c = Connector({ address: 'investments' })
// Connection to address [investments] enstablished

c.transfer(300)
//Transfered an amount of [300] to the address [investments]
Enter fullscreen mode Exit fullscreen mode

Probing & Double Getter

ATTACK

Suppose you are a malicious user of the script. You want to send a sum of money to an address not included in validAddresses.

A frontal attack is obviously blocked.

Connector({ address: 'malicious' })
// The address malicious is not valid. Valid ones are: partner-account, investments, mutual
Enter fullscreen mode Exit fullscreen mode

Remember, while impersonating the hacker you are not aware of the code implementation!

It is possible to send a valid address in advance and count the number of times it is accessed. This way you can tell when it's the right time to - ZAC! - turn it into the malicious address!

Build a probe:

let i = 0
const probe = {
  get address() {
    console.count('probe')
    return 'investments'
  },
}

const c = Connector(probe)
// probe: 1
// probe: 2
// probe: 3
// probe: 4
// Connection to address [investments] enstablished

c.transfer(300)
// probe: 5
Enter fullscreen mode Exit fullscreen mode

It's clear. Just change the fifth reading of address; its validity is checked in the previous four readings. It is possible using the Double Getter technique.

let i = 0
const doubleGetter = {
  get address() {
    if (++i === 5) return 'malicious'
    return 'investments'
  },
}

const c = Connector(doubleGetter)
// Connection to address [investments] enstablished

c.transfer(300)
// Transfered an amount of [300] to the address [malicious]
Enter fullscreen mode Exit fullscreen mode

Thanks to this technique you have effectively bypassed the guards of the initialization phase.

DEFENSE

The problem is that address is repeatedly accessed. Even two would be too many.
But if it were just one, Double Getterss could not fool the guards.

To access address once, simply copy it to a variable. Since it is a string it is primitive - the new variable is a separate copy, without the getter.

In ES6 you can use destructuring:

function Connector({ address }) { ... }
Enter fullscreen mode Exit fullscreen mode

Run the probe and see that it actually beeps only once. The Double Getter threat is neutralized.


Prototype bribing

ATTACK

You have to find a way to infiltrate the code. But they raised the walls - we need an infiltrator, someone from inside who for a moment, just a moment, pretends not to see.

The includes function is your man. Bribing it is simple:

const includesBackup = Array.prototype.includes

// bribe it...
Array.prototype.includes = () => true

const c = Connector({ address: 'malicious' })
// Connection to address [malicious] enstablished

// ...and immediately everything in the norm
Array.prototype.includes = includesBackup

c.transfer(300)
// Transfered an amount of [300] to the address [malicious]
Enter fullscreen mode Exit fullscreen mode

Only during the initialization phase will includes return true indiscriminately. The discriminant guard validAddresses.include(address) is effectively blinded and the malicious address can arrogantly enter through the front door.

DEFENCE

A wall is pulled around the Connector, that is a block scope. Within this you want to have your own copy of Array.prototype.includes that is not corruptible from the outside and use only this one.

{
  const safeIncludes = Array.prototype.includes

  function Connector({ address }) {
    const validAddresses = ['partner-account', 'investments', 'mutual']

    ...

    const isValidAddress = safeIncludes.bind(validAddresses)
    if (!isValidAddress(address)) _err2(address, validAddresses)

    ...
  }

  global.Connector = Connector // window if browser
}
Enter fullscreen mode Exit fullscreen mode

The same trick we used earlier this time will not work and the _err2 will be thrown.

ATTACK

With a little cunning it is possible to corrupt the includes supervisor. This is bind.
I recommend keeping a copy of the corrupt function to get things right as soon as the offense is committed.

const includesBackup = Array.prototype.includes
const bindBackup = Function.prototype.bind

Array.prototype.includes = () => true
Function.prototype.bind = () => () => true

const c = Connector({ address: 'malicious' })
// Connection to address [malicious] enstablished

Array.prototype.includes = includesBackup
Function.prototype.bind = bindBackup

c.transfer(300)
// Transfered an amount of [300] to the address [malicious]
Enter fullscreen mode Exit fullscreen mode

Once again, you managed to evade the guards.


Primitive Illusion

The Connector instance provides thetransfer method. This requires the amount argument which is a number and for the transfer to be successful, it must not exceed the value 500. Suppose I had already managed to establish contact with an address of my choice. At this point I want to transfer a higher amount than allowed.

// Connector#transfer
function transfer(amount) {
  if (!amount || amount <= 0) _err3()

  if (amount > 500) _err4()

  console.info(
    `Transfered an amount of [${amount}] to the address [${options.address}]`
  )
}
Enter fullscreen mode Exit fullscreen mode

The Primitive Illusion technique achieves an effect similar to the Double Getter but in other ways. A limitation of the DG technique is in fact that of being applicable only to variables passed by reference. Try to implement it for a primitive - Number for example.

I find it more functional to modify Number.prototype.valueOf. This is a method you will probably never need to call directly. JavaScript itself invokes it when it needs to retrieve the primitive value of an object (in this case, a Number). Intuition is more likely with an example:

Number.prototype.valueOf = () => {
  console.count('probe')
  return this
}
Enter fullscreen mode Exit fullscreen mode

this in the case of Number represents the same number passed in the constructor.

You probably recognized it, it's a probe. You test different operations on an instance of Number:

const number = new Number(42)

console.log(number)
// [Number: 42]

console.log(+number)
// probe: 1
// 42

console.log(number > 0)
// probe: 2
// true
Enter fullscreen mode Exit fullscreen mode

As you guess on the fly, the valueOf method is invoked when primitive value is expected - as in the case of a mathematical operation. At this point all that remains is to insert the probe into the transfer method.

c.transfer(number)
// probe: 1
// probe: 2
// Transfered an amount of [42] to the address [hacker-address]
Enter fullscreen mode Exit fullscreen mode

The two logs of the probe correspond precisely in amount <= 0 andamount> 500. At this point you realize that you don't need to swap the value for another at some point - you just need to return a value that satisfies the above conditions when valueOf is called.

Number.prototype.valueOf = () => 1
const number = new Number(100000)

c.transfer(number)
// Transfered an amount of [100000] to the address [hacker-address]
Enter fullscreen mode Exit fullscreen mode

Again, you managed to get what you wanted.


If you want to chat about nerdy things or just say hi, you can find me here:

Top comments (5)

Collapse
 
wsh4and profile image
Eko Andri Subarnanto

Do you have a Udemy courses covering topics like this one?
I want to learn more about it, this is gold, loved it.

Collapse
 
didof profile image
Francesco Di Donato

Hi Eko, I do not. But I am very passionate about this topic, and I intend to release a course in the future.
I'm working on it, wish me luck :)

Collapse
 
joseooortega profile image
Jose Carlos Ortega Alves

Could you post here the course when it's available? I'm interested too :)

Thread Thread
 
didof profile image
Francesco Di Donato

Of course, will be my pleasure :)

Collapse
 
wsh4and profile image
Eko Andri Subarnanto

Looking forward for it!!
Best of luck