orbitdb / orbitdb

Peer-to-Peer Databases for the Decentralized Web

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Custom AccessController is not immutable

shideneyu opened this issue · comments

class CustomAccessController {
    static get type() { return 'someUniqId' }

    static async create(orbitdb, options = {}) {
      return new CustomAccessController(orbitdb, options);
    }

    async canAppend(entry, identityProvider) {
        return true;
    }
  
    async load(address) {}
    async save() { return {} }
}

AccessControllers.addAccessController({ AccessController: CustomAccessController });

const ipfsOptions = {
    repo: 'ipfs/' + 'someUniqId',
    config: {
        Addresses: {
            Swarm: [
                '/dns4/wrtc-star2.sjc.dwebops.pub/tcp/443/wss/p2p-webrtc-star/'
            ]
        }
    }
};

const ipfs = await Ipfs.create(ipfsOptions);

const orbitdb = await OrbitDB.createInstance(ipfs, {
    AccessControllers: AccessControllers
});

const db = await orbitdb.keyvalue('someUniqId', {
    accessController: {
        type: CustomAccessController.type,
    }
});
await db.load();
await db.put('key', 'value');

Hello, for the sake of understanding, I've minified my code a maximum.

With this chunk of Code I've successfully added { 'key', 'value' } on the keyValue OrbitDB database called someUniqId.
The issue arises when I create a new OrbitDB database with a new uniq name someUniqId2, that this time I set true to false to the method canAppend

    async canAppend(entry, identityProvider) {
        return false;
    }

The first time I run this code, I get an error since the user is not allowed to update it.

The issue is : If a second user (user number 2) runs after this the same code but with return true instead of return false, he can access and perform the db.puts successfully on the database someUniqId2.

I thought that my CustomAccessController is forever added in a immutable way to my new someUniqId2 database the first time the db gets created. I thought that Custom Access Controllers were created as to define forever during the creation of the databases the rules set ? Is my understanding incorrect ? How can user number 2 overrides the custom access controller rules and managed to propagates the put entry to OrbitDB/IPFS ? Been struggling a day with this issue.

In my real code (not this simplified version), I've set complex rules. 4 first append gets accepted. The fifth one gets refused. But a malicious user just needs to replace the whole canAppend function by return true; to bypass all the verifications.

Thanks

Could you provide the version of orbitdb you are using? The example code above looks very outdated. For example, there is no addAccessController function available in the latest version of OrbitDB.

You can install the latest version of OrbitDB using npm i @orbitdb/core.

Thank you four reply. I took some time to fix my code as to use the latest version of orbitdb (1.0.0) and show you the bug:

import { create } from 'ipfs-core'
import { createOrbitDB, useAccessController } from '@orbitdb/core'

const CustomAccessController = () => async ({ orbitdb, identities, address }) => {
  address = '/custom/access-controller';

  return {
    address,
    canAppend: (entry, identityProvider) => {
      if(entry.payload.key == 'forbiddenKey') {
        return false;
      }
      else {
        return true;
      }
    }
  };
};

CustomAccessController.type = 'custom';

useAccessController(CustomAccessController);

var ipfs = await create();
var orbitdb = await createOrbitDB({ ipfs });

const db = await orbitdb.open('someUniqIdenifier', {
  type: 'keyvalue',
  AccessController: CustomAccessController(),
});

console.log("db address:", db.address);

await db.put('myFirstKey', 'ok');
try {
  await db.put('forbiddenKey', 'ok');
}
catch {
  console.log('Error ! Forbidden');
}

var allEntries = await db.all();

for (const [key, value] of Object.entries(allEntries)) {
  console.log(key, value);
}

await db.close()
await orbitdb.stop()
await ipfs.stop()

// Malicious code starts here

const MyMaliciousAccessController = () => async ({ orbitdb, identities, address }) => {
  address = '/custom/access-controller';

  return {
    address,
    canAppend: (entry, identityProvider) => {
      return true;
    }
  };
};

MyMaliciousAccessController.type = 'custom';


useAccessController(MyMaliciousAccessController);

var ipfs = await create();
var orbitdb = await createOrbitDB({ ipfs });

const secondDb = await orbitdb.open(db.address, {
  type: 'keyvalue',
  AccessController: MyMaliciousAccessController(),
});

await secondDb.put('forbiddenKey', 'ok');

var allEntries = await secondDb.all();

for (const [key, value] of Object.entries(allEntries)) {
  console.log(key, value);
}

So here I created a custom access controller called CustomAccessController. There is just one rule: every keys are accepted but not forbiddenKey.
It works perfectly.

But if a malicious user wants to add this key, he just needs to clone all the code and set returns true; in the canAppend function of his own malicious access controller.

I thought that the AccessController was immutable once created and that new users are forced to use it. Did I miss something ?

The doc states:

The access information cannot be changed after the initial setup (as it is immutable). If different write access is needed, you will need to set up a new database and associated IPFSAccessController. It is important to note that this will change the address of the database.

Thank you very much for your support and hard work 🙏 I really hope we can find a solution to this issue. I really hope that we can prevent users from editing databases if they use malicious access controller. Otherwise, accesscontrollers would be as good as writing a if condition before a db.put/add :-(

Hi, I was reading through docs/issues today preparing to use OrbitDB and noticed this:

https://github.com/orbitdb/orbitdb/blob/main/docs/ACCESS_CONTROLLERS.md

OrbitDB is bundled with two AccessControllers; IPFSAccessController, an immutable access controller which uses IPFS to store the access settings, and OrbitDBAccessController, a mutable access controller which uses OrbitDB's keyvalue database to store one or more permissions.

I'm very interested in this and thanks for working on it. Two things I noticed:

  1. Are you using the same identity when you make the secondDb? From the code it would seem your user has write access.

  2. Client code in the wild will always be malicious. There's nothing to stop a client from writing malicious data for itself and pushing it out to the network.

The problem would be if a non-malicious client reads and accepts the malicious data.

I haven't dug into this, but it looks like tests have been written around these use cases: https://github.com/orbitdb/orbitdb/blob/main/test/access-controllers/orbit-db-access-controller.test.js#L240

Hello
Thanks for participating to this issue
You can copy paste the entire code at #1122 (comment)
Put it into bug.mjs and run node bug.mjs, and you will be able to better understand what is happening. You can even use it to help you write your own code.

Are you using the same identity when you make the secondDb? From the code it would seem your user has write access.

By default, it's as if we use AccessController({ write: ['*'] }), meaning that everyone has write access.
In my code I did not specify any identity. Meaning that the identities are different for the two dbs I guess ?

When you use identities, you can then read the identity in the canAppend function of the access controller to then verify who sent the message.

I haven't dug into this, but it looks like tests have been written around these use cases: https://github.com/orbitdb/orbitdb/blob/main/test/access-controllers/orbit-db-access-controller.test.js#L240

These use cases are unfortunately different from what I want to achieve. grant and revoke authorize identities to write or not into the db.

In my example, everyone can write in the db. But I want to prevent some information to be written.

Actually my real usecase is a bit different, that example was made to make it simple to debug. My real usecase is:

I want each identity to be able to write and update their own message on the same DB. But I don't want other identities to be able to overwrite or delete the messages made by other identities.

I tried to make an example as simple as possible. My real code has custom identity providers, and custom access controllers. I even managed to read the database from the access controller.

But my whole project is void of interest if people can overwrite other's message. I'm not sure why I'm the first to encounter this issue. My usecase is very basic I think.

So in the end, this is exactly the title of this issue: Custom AccessController is not immutable. I hope I am wrong.

Given what I know, I'd implement that as a database of databases. Each user having a database with write access, and some central index of databases. That's more like distributed database-per-user in couchdb/pouchdb than a central mysql server.

I think OpenMLS can support per-message-auth-integrity through groups, but it's a very heavyweight solution, and I don't know an IPFS or javascript implementation.

I'm new here, what do yall think?

The more I dig to the rabbit hole, the more I come to the realization that no amount of effort can help us since a user can just monkey patch OrbitDB alltogether to bypass all securities related procedures and grant themselves ever possible rights since we can open a database with our own malicious access controller and identities.

Because the access controllers and identities manifest are published to IPFS but what enforces their usages are not the IPFS nodes but this very library.

If someone monkey patches this everything is over: https://github.com/orbitdb/orbitdb/blob/main/src/orbitdb.js#L127

And I think I finally saw on why just creating a malicious access controller works, it's due to this line here: https://github.com/orbitdb/orbitdb/blob/main/src/access-controllers/index.js#L39

My malicious controller has the same type has the original one. It simply overrides it.

But even if we patch this, nothing prevents a malicious user from polluting or corrupting the database by evading the identity and access controllers rules, by patching their own OrbitDB library or using said exploit I revealed, is that right @haydenyoung ?

In other words I'm afraid that any keyValue database can be deleted or updated by anyone even if their associated manifest has rules that govern their writing rights. Because only front code that can be changed by the user prevents them from opening a database with a different manifest.

@shideneyu Like I said in my first comment, that's every web application ever written.

"Never trust the client" is the first rule of client-server security.
"Never trust a peer" is the first rule of p2p security.

My expectation would be the "good" peer rejects the new data from the "bad" peer, and that "good" peers can know what's good from the IPFS data, not the code.

That is right. Unfortunately for the keyValue database, I don't think that a good peer can know what was the previous "valid" value for a key if it got updated or deleted ?

Maybe if we sign every messages and check the oplog. I'll dig this

When writing malicious records to secondDb, check whether firstDb is replicating them. If not, the access controllers should be working as expected and your firstDb should be "protected" from any data being written to secondDb.

There is nothing stopping secondUser from circumventing (for whatever reason) local checks on secondDb. firstUser is not responsible for ensuring the integrity of secondUser's data. Instead, we want to ensure that firstDb will not replicate secondDb's data if it is in violation of the rules laid out by the Custom AC.

We want to ensure that firstDb will not replicate secondDb's data if it is in violation of the rules laid out by the Custom AC.

This is what I was expecting. But I can't replicate this scenario. Can you show me how that works @haydenyoung ?

In my code, firstDb is replicating secondDb's data. You just need to run the code twice and you'll see on the second firstDb.all() that it fetched the violating data.

image

Or you can just run this: node.mjs https://gist.github.com/shideneyu/31e77772ec2a14f2746252720efdbd5e

@shideneyu can you confirm that firstDb is running the "good" custom AC and secondDB is running the malicious one? I.e. firstDb isn't running the malicious AC?

If it is running correctly, firstDb should be throwing "cannot write" errors when it tries to replicate secondDb's unwanted record.

@haydenyoung yes that is the issue. canAppend() is not used when replicating dbs.
firstDb is not throwing "cannot write" errors when it replicates secondDb's unwanted record.

To make it simple, you can reproduce it by running those three files one by one:

node create_with_db1.mjs , node append_with_db2.mjs, node replicate_with_db1.mjs

create_with_db1.mjs , append_with_db2.mjs , replicate_with_db1.mjs

image

It looks like db1 and db2 are pointing at the same database. Make sure you instantiate two separate ipfs nodes and two separate orbitdb peers by using different paths (I.e. this is how it would look if deployed "in the wild", i.e. two separate nodes on two separate servers/browsers).

I have attached two separate scripts which should give you a better idea how to sync two peers and how peer 1 guarantees it does not write the malicious record using the correct CustomAC.
db1.js.txt
db2.js.txt

Firstly, remove the ".txt" suffix from the db1 and db2 js files (or rename suffix to mjs).

Run db1.js in the terminal:

node db1.js

this will initialize your orbitdb node in a separate directory (./orbitdb/1). It will also print out the database address (will look something like /orbitdb/). Leave db1.js running so that db2.js can connect to it.

Run db2.js in a separate terminal, using db1's address as the address for db2 (I.e. a replicating db):

node db2.js /orbitdb/<hash-that-you-copied-from-db-1>

db2 will successfully write a record to its own log using the malicious AC but db1 will throw a "not allowed to write" error, not allowing the malicious record to be written to db1's log.

Hope this helps.

I understand better now, this helps a lot thanks 🙏

Make sure you instantiate two separate ipfs nodes and two separate orbitdb peers by using different paths (I.e. this is how it would look if deployed "in the wild", i.e. two separate nodes on two separate servers/browsers).

My understanding is a bit limited though. In my context I wanted to create a live chat. I did not expect to be hosting nodes since I wanted a decentralized application hosted on IPFS (just plain html / js).

If all of the users messaging each others on that website become peers (ip4/127.0.0.1/tcp/0), if they all close their computers, a new user will not be able to replicate db1 is that right ?

Does this mean that the guy who created db1 with the right Controller Access needs to stay up 24/24 ?
Or does it mean I need to maintain my own IPFS nodes that people can connect to ? (it would become centralized then)

How does your solution works with a scenario where there is no special nodes and that the data is stored on IPFS (maybe with webrtc stars nodes) ?

If all of the users messaging each others on that website become peers (ip4/127.0.0.1/tcp/0), if they all close their computers, a new user will not be able to replicate db1 is that right ?

Yes, that's the nature of p2p. The peers are providing the data but there's no guarantee that they will be available. Hence, OrbitDB strives to make every replica of the db eventually consistent despite the node unreliability. However, what would be the reasoning for a new user be replicating the chat log when everyone else has left the chat?

Does this mean that the guy who created db1 with the right Controller Access needs to stay up 24/24 ?

Not necessarily. If one of the above peers is available, the new user can dial into one of these peers to replicate the data.

The AC type is retrieved as part of the db manifest (db description). The AC will be the one used by OrbitDB; either one of the bundled IPFS or OrbitDB or a custom AC. In the case of the examples you gave above, this would be the custom AC you have deployed as part of your chat app.

How does your solution works with a scenario where there is no special nodes and that the data is stored on IPFS (maybe with webrtc stars nodes) ?

I'm not entirely sure I understand what "special nodes" are.

Since the db's manifest and entries are stored using IPFS storage I guess you could potentially pin each block remotely but you would still need to re-index the database entries and determine the db heads when replicating on another peer. I've never tried this so I'm not even sure if retrieving data from remotely pinned IPFS blocks is even feasible.

@shideneyu If you are looking for guidance or would like to discuss developing your decentralized ideas further, I would recommend engaging the OrbitDB community. They may be able to give you more insight regarding implementation of OrbitDB.

Or does it mean I need to maintain my own IPFS nodes that people can connect to ? (it would become centralized then)

Well I think centralization is a sliding scale. Let's say you have 10 peers and one of them is up all the time. Provided the other peers are not completely reliant on this peer I think all you have is a decentralized app with a really reliable peer.

@shideneyu I'm going to close this issue as I think it is resolved. However, if you have any more questions, feel free to re-open this issue or pose any questions you have to the community on the OrbitDB Matrix/Gitter channels.