Skip to content

Commit 85fe071

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

File tree

2 files changed

+91
-36
lines changed

2 files changed

+91
-36
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 6 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: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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);
@@ -928,19 +906,54 @@ mocha.describe('bucketspace_fs', function() {
928906
});
929907

930908
mocha.describe('bucket tagging operations', function() {
909+
const test_bucket_tagging = 'test-bucket-tagging';
910+
mocha.before(async function() {
911+
await create_bucket(test_bucket_tagging);
912+
});
913+
931914
mocha.it('put_bucket_tagging', async function() {
932-
const param = { name: test_bucket, tagging: [{ key: 'k1', value: 'v1' }] };
915+
const param = { name: test_bucket_tagging, tagging: [{ key: 'k1', value: 'v1' }] };
933916
await bucketspace_fs.put_bucket_tagging(param, dummy_object_sdk);
934917
const tag = await bucketspace_fs.get_bucket_tagging(param);
935918
assert.deepEqual(tag, { tagging: param.tagging });
936919
});
937920

938921
mocha.it('delete_bucket_tagging', async function() {
939-
const param = { name: test_bucket };
922+
const param = { name: test_bucket_tagging };
940923
await bucketspace_fs.delete_bucket_tagging(param);
941924
const tag = await bucketspace_fs.get_bucket_tagging(param);
942925
assert.deepEqual(tag, { tagging: [] });
943926
});
927+
928+
mocha.it('put_bucket_tagging - different account with bucket policy (principal by name)', async function() {
929+
const policy = generate_s3_policy(account_user4.name, test_bucket_tagging, ['s3:PutBucketTagging']).policy;
930+
const param = { name: test_bucket_tagging, policy: policy };
931+
await bucketspace_fs.put_bucket_policy(param);
932+
const dummy_object_sdk_for_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_user4);
933+
const tagging_param = { name: test_bucket_tagging, tagging: [{ key: 'test-key', value: 'test-value' }] };
934+
await bucketspace_fs.put_bucket_tagging(tagging_param, dummy_object_sdk_for_account);
935+
const tagging = await bucketspace_fs.get_bucket_tagging({ name: test_bucket_tagging });
936+
assert.equal(tagging.tagging.length, 1);
937+
assert.equal(tagging.tagging[0].key, 'test-key');
938+
});
939+
940+
mocha.it('put_bucket_tagging - different account with bucket policy (principal by id)', async function() {
941+
const policy = generate_s3_policy(account_user4._id, test_bucket_tagging, ['s3:PutBucketTagging']).policy;
942+
const param = { name: test_bucket_tagging, policy: policy };
943+
await bucketspace_fs.put_bucket_policy(param);
944+
const dummy_object_sdk_for_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_user4);
945+
const tagging_param = { name: test_bucket_tagging, tagging: [{ key: 'test-key', value: 'test-value' }] };
946+
await bucketspace_fs.put_bucket_tagging(tagging_param, dummy_object_sdk_for_account);
947+
const tagging = await bucketspace_fs.get_bucket_tagging({ name: test_bucket_tagging });
948+
assert.equal(tagging.tagging.length, 1);
949+
assert.equal(tagging.tagging[0].key, 'test-key');
950+
});
951+
952+
mocha.after(async function() {
953+
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket_tagging}`);
954+
const file_path = get_config_file_path(CONFIG_SUBDIRS.BUCKETS, test_bucket_tagging);
955+
await fs_utils.file_delete(file_path);
956+
});
944957
});
945958

946959
mocha.describe('bucket lifecycle operations', function() {

0 commit comments

Comments
 (0)