blog
javascript

Penetration and Security in JavaScript

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. Using checks is not enough.


Written on October 7, 2021. Originally posted on dev.to .

This post got inspired by this Thomas Hunter II .

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}]`
    )
  }
}

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]

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

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

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]

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 }) { ... }

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]

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
}

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]

Once again, you managed to evade the guards.


Primitive Illusion

The Connector instance provides the transfer 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}]`
  )
}

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
}

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

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]

The two logs of the probe correspond precisely in amount <= 0 and amount> 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]

Again, you managed to get what you wanted.