Skip to content

Commit 0e22c56

Browse files
Fix (NC | NSFS) - list_buckets allowing unauthorized bucket access
Signed-off-by: Aayush Chouhan <[email protected]>
1 parent 0d80b71 commit 0e22c56

File tree

2 files changed

+55
-35
lines changed

2 files changed

+55
-35
lines changed

src/sdk/bucketspace_fs.js

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
241241
* permissions
242242
* a. First iteration - map_with_concurrency with concurrency rate of 10 entries at the same time.
243243
* a.1. if entry is dir file/entry is non json - we will return undefined
244-
* a.2. if bucket is unaccessible by bucket policy - we will return undefined
244+
* a.2. if bucket is not owned by the account (or their root account for IAM users) - we will return undefined
245245
* a.3. if underlying storage of the bucket is unaccessible by the account's uid/gid - we will return undefined
246246
* a.4. else - return the bucket config info.
247247
* b. Second iteration - filter empty entries - filter will remove undefined values produced by the map_with_concurrency().
@@ -271,9 +271,9 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
271271
if (err.rpc_code !== 'NO_SUCH_BUCKET') throw err;
272272
}
273273
if (!bucket) return;
274-
const bucket_policy_accessible = await this.has_bucket_action_permission(bucket, account, 's3:ListBucket');
275-
dbg.log2(`list_buckets: bucket_name ${bucket_name} bucket_policy_accessible`, bucket_policy_accessible);
276-
if (!bucket_policy_accessible) return;
274+
275+
if (!await this.has_bucket_ownership_permission(bucket, account)) return;
276+
277277
const fs_accessible = await this.validate_fs_bucket_access(bucket, object_sdk);
278278
if (!fs_accessible) return;
279279
return bucket;
@@ -921,24 +921,66 @@ class BucketSpaceFS extends BucketSpaceSimpleFS {
921921
///// UTILS /////
922922
/////////////////
923923

924-
// TODO: move the following 3 functions - has_bucket_action_permission(), validate_fs_bucket_access(), _has_access_to_nsfs_dir()
924+
// TODO: move the following 5 functions - is_bucket_owner(), is_iam_and_same_root_account_owner(), has_bucket_ownership_permission(), has_bucket_action_permission(), validate_fs_bucket_access(), _has_access_to_nsfs_dir()
925925
// so they can be re-used
926+
927+
/**
928+
* is_bucket_owner checks if the account is the direct owner of the bucket
929+
* @param {Record<string, any>} bucket
930+
* @param {Record<string, any>} account
931+
* @returns {boolean}
932+
*/
933+
is_bucket_owner(bucket, account) {
934+
if (!bucket?.owner_account?.id || !account?._id) return false;
935+
return bucket.owner_account.id === account._id;
936+
}
937+
938+
/**
939+
* is_iam_and_same_root_account_owner checks if the account is an IAM user and the bucket is owned by their root account
940+
* @param {Record<string, any>} account
941+
* @param {Record<string, any>} bucket
942+
* @returns {boolean}
943+
*/
944+
is_iam_and_same_root_account_owner(account, bucket) {
945+
if (!account?.owner || !bucket?.owner_account?.id) return false;
946+
return account.owner === bucket.owner_account.id;
947+
}
948+
949+
/**
950+
* has_bucket_ownership_permission returns true if the account can list the bucket in ListBuckets operation
951+
*
952+
* aws-compliant behavior:
953+
* - Root accounts can list buckets they own
954+
* - IAM users can list their owner buckets
955+
*
956+
* @param {Record<string, any>} bucket
957+
* @param {Record<string, any>} account
958+
* @returns {Promise<boolean>}
959+
*/
960+
async has_bucket_ownership_permission(bucket, account) {
961+
// check direct ownership
962+
if (this.is_bucket_owner(bucket, account)) return true;
963+
964+
// special case: iam user can list the buckets of their owner
965+
if (this.is_iam_and_same_root_account_owner(account, bucket)) return true;
966+
967+
return false;
968+
}
969+
926970
async has_bucket_action_permission(bucket, account, action, bucket_path = "") {
927971
dbg.log1('has_bucket_action_permission:', bucket.name.unwrap(), account.name.unwrap(), account._id, bucket.bucket_owner.unwrap());
928972

929-
const is_system_owner = Boolean(bucket.system_owner) && bucket.system_owner.unwrap() === account.name.unwrap();
973+
// system_owner is deprecated since version 5.18, so we don't need to check it
974+
975+
// check ownership: direct owner or IAM user inheritance
976+
const is_owner = this.is_bucket_owner(bucket, account);
930977

931-
// If the system owner account wants to access the bucket, allow it
932-
if (is_system_owner) return true;
933-
const is_owner = (bucket.owner_account && bucket.owner_account.id === account._id);
934978
const bucket_policy = bucket.s3_policy;
935979

936980
if (!bucket_policy) {
937981
// in case we do not have bucket policy
938982
// we allow IAM account to access a bucket that that is owned by their root account
939-
const is_iam_and_same_root_account_owner = account.owner !== undefined &&
940-
account.owner === bucket.owner_account.id;
941-
return is_owner || is_iam_and_same_root_account_owner;
983+
return is_owner || this.is_iam_and_same_root_account_owner(account, bucket);
942984
}
943985
if (!action) {
944986
throw new Error('has_bucket_action_permission: action is required');

src/test/unit_tests/nsfs/test_bucketspace_fs.js

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const nb_native = require('../../../util/nb_native');
2222
const SensitiveString = require('../../../util/sensitive_string');
2323
const NamespaceFS = require('../../../sdk/namespace_fs');
2424
const BucketSpaceFS = require('../../../sdk/bucketspace_fs');
25-
const { TMP_PATH, generate_s3_policy } = require('../../system_tests/test_utils');
25+
const { TMP_PATH } = require('../../system_tests/test_utils');
2626
const { CONFIG_SUBDIRS, JSON_SUFFIX } = require('../../../sdk/config_fs');
2727
const nc_mkm = require('../../../manage_nsfs/nc_master_key_manager').get_instance();
2828

@@ -519,28 +519,6 @@ mocha.describe('bucketspace_fs', function() {
519519
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account);
520520
assert.equal(res.buckets.length, 0);
521521
});
522-
mocha.it('list buckets - different account with bucket policy (principal by name)', async function() {
523-
// another user created a bucket
524-
// with bucket policy account_user3 can list it
525-
const policy = generate_s3_policy(account_user4.name, test_bucket, ['s3:*']).policy;
526-
const param = { name: test_bucket, policy: policy };
527-
await bucketspace_fs.put_bucket_policy(param);
528-
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_user4);
529-
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account);
530-
assert.equal(res.buckets.length, 1);
531-
assert.equal(res.buckets[0].name.unwrap(), test_bucket);
532-
});
533-
mocha.it('list buckets - different account with bucket policy (principal by id)', async function() {
534-
// another user created a bucket
535-
// with bucket policy account_user3 can list it
536-
const policy = generate_s3_policy(account_user4._id, test_bucket, ['s3:*']).policy;
537-
const param = { name: test_bucket, policy: policy };
538-
await bucketspace_fs.put_bucket_policy(param);
539-
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_user4);
540-
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account);
541-
assert.equal(res.buckets.length, 1);
542-
assert.equal(res.buckets[0].name.unwrap(), test_bucket);
543-
});
544522
mocha.afterEach(async function() {
545523
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket}`);
546524
const file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket);

0 commit comments

Comments
 (0)