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:
- Probing & Double Getter
- Prototype Bribing
- 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 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}]`
)
}
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 ofNumber
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
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]
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)
Do you have a Udemy courses covering topics like this one?
I want to learn more about it, this is gold, loved it.
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 :)
Could you post here the course when it's available? I'm interested too :)
Of course, will be my pleasure :)
Looking forward for it!!
Best of luck