Skip to content

Commit 0c47879

Browse files
Fix (NC | NSFS) - list_buckets allowing unauthorized bucket access
Signed-off-by: Aayush Chouhan <achouhan@redhat.com>
1 parent 7e70923 commit 0c47879

File tree

2 files changed

+115
-32
lines changed

2 files changed

+115
-32
lines changed

src/sdk/bucketspace_fs.js

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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_list_buckets_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,23 +921,101 @@ 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 4 functions - is_bucket_owner(), has_list_buckets_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+
// Ensure both IDs exist to avoid undefined === undefined bug
935+
const bucket_owner_id = bucket?.owner_account?.id;
936+
const account_id = account?._id;
937+
return bucket_owner_id !== undefined && bucket_owner_id === account_id;
938+
}
939+
940+
/**
941+
* has_iam_list_buckets_permission checks if the account has IAM policy granting s3:ListAllMyBuckets
942+
* for buckets owned by their root account
943+
* @param {Record<string, any>} bucket
944+
* @param {Record<string, any>} account
945+
* @returns {Promise<boolean>}
946+
*/
947+
async has_iam_list_buckets_permission(bucket, account) {
948+
// only IAM users can have IAM policies
949+
if (!account?.owner) return false;
950+
951+
const iam_policies = account.iam_user_policies || [];
952+
if (iam_policies.length === 0) return false;
953+
954+
// check each IAM policy for s3:ListAllMyBuckets permission
955+
for (const iam_policy of iam_policies) {
956+
const result = await bucket_policy_utils.has_bucket_policy_permission(
957+
iam_policy.policy_document,
958+
undefined, // no principal check for IAM policies
959+
's3:listallmybuckets',
960+
'arn:aws:s3:::*',
961+
undefined,
962+
{ should_pass_principal: false } // skip principal check
963+
);
964+
965+
if (result === 'DENY') return false;
966+
967+
// if policy allows, check if bucket is owned by the IAM user's root account
968+
// IAM policy only grants access to buckets owned by the IAM user's root account
969+
if (result === 'ALLOW') {
970+
const bucket_owner_id = bucket?.owner_account?.id;
971+
return bucket_owner_id !== undefined && bucket_owner_id === account.owner;
972+
}
973+
}
974+
975+
return false;
976+
}
977+
978+
/**
979+
* has_list_buckets_permission returns true if the account can list the bucket in ListBuckets operation
980+
* only checks bucket ownership
981+
*
982+
* aws-compliant behavior:
983+
* - Root accounts can list buckets they own
984+
* - IAM users cannot inherit ListBuckets visibility from root (unlike bucket operations)
985+
* - IAM users need explicit IAM policy with s3:ListAllMyBuckets to list buckets
986+
*
987+
* @param {Record<string, any>} bucket
988+
* @param {Record<string, any>} account
989+
* @returns {Promise<boolean>}
990+
*/
991+
async has_list_buckets_permission(bucket, account) {
992+
// check direct ownership (root accounts only)
993+
if (this.is_bucket_owner(bucket, account)) return true;
994+
995+
// check IAM policy for s3:ListAllMyBuckets
996+
// Note: IAM users need this policy to list buckets owned by their root account
997+
if (await this.has_iam_list_buckets_permission(bucket, account)) return true;
998+
999+
return false;
1000+
}
1001+
9261002
async has_bucket_action_permission(bucket, account, action, bucket_path = "") {
9271003
dbg.log1('has_bucket_action_permission:', bucket.name.unwrap(), account.name.unwrap(), account._id, bucket.bucket_owner.unwrap());
9281004

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

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);
9341010
const bucket_policy = bucket.s3_policy;
9351011

9361012
if (!bucket_policy) {
9371013
// in case we do not have bucket policy
9381014
// we allow IAM account to access a bucket that that is owned by their root account
1015+
const bucket_owner_id = bucket.owner_account?.id;
9391016
const is_iam_and_same_root_account_owner = account.owner !== undefined &&
940-
account.owner === bucket.owner_account.id;
1017+
bucket_owner_id !== undefined &&
1018+
account.owner === bucket_owner_id;
9411019
return is_owner || is_iam_and_same_root_account_owner;
9421020
}
9431021
if (!action) {

src/test/unit_tests/nsfs/test_bucketspace_fs.js

Lines changed: 28 additions & 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

@@ -271,6 +271,17 @@ const account_iam_user1 = {
271271
gid: dummy_object_sdk.requesting_account.nsfs_account_config.gid,
272272
new_buckets_path: dummy_object_sdk.requesting_account.nsfs_account_config.new_buckets_path
273273
},
274+
iam_user_policies: [{
275+
policy_name: 'ListBucketsPolicy',
276+
policy_document: {
277+
Version: '2012-10-17',
278+
Statement: [{
279+
Effect: 'Allow',
280+
Action: 's3:ListAllMyBuckets',
281+
Resource: '*'
282+
}]
283+
}
284+
}],
274285
creation_date: '2023-11-30T04:46:33.815Z',
275286
};
276287

@@ -291,6 +302,17 @@ const account_iam_user2 = {
291302
gid: dummy_object_sdk.requesting_account.nsfs_account_config.gid,
292303
new_buckets_path: dummy_object_sdk.requesting_account.nsfs_account_config.new_buckets_path
293304
},
305+
iam_user_policies: [{
306+
policy_name: 'ListBucketsPolicy',
307+
policy_document: {
308+
Version: '2012-10-17',
309+
Statement: [{
310+
Effect: 'Allow',
311+
Action: 's3:ListAllMyBuckets',
312+
Resource: '*'
313+
}]
314+
}
315+
}],
294316
creation_date: '2023-12-30T04:46:33.815Z',
295317
};
296318

@@ -501,13 +523,13 @@ mocha.describe('bucketspace_fs', function() {
501523
});
502524
mocha.it('list buckets - iam accounts', async function() {
503525
// root account created a bucket
504-
// account_iam_user2 can list the created bucket (the implicit policy - same root account)
526+
// account_iam_user1 can list the created bucket (has s3:ListAllMyBuckets policy)
505527
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1);
506528
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account);
507529
assert.equal(res.buckets.length, 1);
508530
assert.equal(res.buckets[0].name.unwrap(), test_bucket);
509531

510-
// account_iam_user2 can list the created bucket (the implicit policy - same root account)
532+
// account_iam_user2 can list the created bucket (has s3:ListAllMyBuckets policy)
511533
const dummy_object_sdk_for_iam_account2 = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user2);
512534
const res2 = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account2);
513535
assert.equal(res2.buckets.length, 1);
@@ -519,27 +541,10 @@ mocha.describe('bucketspace_fs', function() {
519541
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_iam_account);
520542
assert.equal(res.buckets.length, 0);
521543
});
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);
544+
mocha.it('list buckets - different account cannot list buckets they do not own', async function() {
539545
const dummy_object_sdk_for_iam_account = make_dummy_object_sdk_for_account(dummy_object_sdk, account_user4);
540546
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);
547+
assert.equal(res.buckets.length, 0);
543548
});
544549
mocha.afterEach(async function() {
545550
await fs_utils.folder_delete(`${new_buckets_path}/${test_bucket}`);
@@ -626,7 +631,7 @@ mocha.describe('bucketspace_fs', function() {
626631
// root account created the bucket
627632
await create_bucket(test_bucket_iam_account);
628633

629-
// account_iam_user1 can see the bucket in the list
634+
// account_iam_user1 can see the bucket in the list (has s3:ListAllMyBuckets policy)
630635
const dummy_object_sdk_for_account_iam_user1 = make_dummy_object_sdk_for_account(dummy_object_sdk, account_iam_user1);
631636
const res = await bucketspace_fs.list_buckets({}, dummy_object_sdk_for_account_iam_user1);
632637
assert.ok(res.buckets.length > 0);

0 commit comments

Comments
 (0)