Merge pull request #6884 from coollabsio/fix-invite-privilege-escalation

fix: critical privilege escalation in team invitation system
This commit is contained in:
Andras Bacsai 2025-10-15 20:56:39 +02:00 committed by GitHub
commit 5c61b27a96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 373 additions and 11 deletions

View file

@ -45,9 +45,16 @@ private function generateInviteLink(bool $sendEmail = false)
try {
$this->authorize('manageInvitations', currentTeam());
$this->validate();
if (auth()->user()->role() === 'admin' && $this->role === 'owner') {
// Prevent privilege escalation: users cannot invite someone with higher privileges
$userRole = auth()->user()->role();
if ($userRole === 'member' && in_array($this->role, ['admin', 'owner'])) {
throw new \Exception('Members cannot invite admins or owners.');
}
if ($userRole === 'admin' && $this->role === 'owner') {
throw new \Exception('Admins cannot invite owners.');
}
$this->email = strtolower($this->email);
$member_emails = currentTeam()->members()->get()->pluck('email');

View file

@ -42,8 +42,7 @@ public function update(User $user, Team $team): bool
return false;
}
// return $user->isAdmin() || $user->isOwner();
return true;
return $user->isAdmin() || $user->isOwner();
}
/**
@ -56,8 +55,7 @@ public function delete(User $user, Team $team): bool
return false;
}
// return $user->isAdmin() || $user->isOwner();
return true;
return $user->isAdmin() || $user->isOwner();
}
/**
@ -70,8 +68,7 @@ public function manageMembers(User $user, Team $team): bool
return false;
}
// return $user->isAdmin() || $user->isOwner();
return true;
return $user->isAdmin() || $user->isOwner();
}
/**
@ -84,8 +81,7 @@ public function viewAdmin(User $user, Team $team): bool
return false;
}
// return $user->isAdmin() || $user->isOwner();
return true;
return $user->isAdmin() || $user->isOwner();
}
/**
@ -98,7 +94,6 @@ public function manageInvitations(User $user, Team $team): bool
return false;
}
// return $user->isAdmin() || $user->isOwner();
return true;
return $user->isAdmin() || $user->isOwner();
}
}

View file

@ -0,0 +1,176 @@
<?php
use App\Livewire\Team\InviteLink;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create a team with owner, admin, and member
$this->team = Team::factory()->create();
$this->owner = User::factory()->create();
$this->admin = User::factory()->create();
$this->member = User::factory()->create();
$this->team->members()->attach($this->owner->id, ['role' => 'owner']);
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
$this->team->members()->attach($this->member->id, ['role' => 'member']);
});
describe('privilege escalation prevention', function () {
test('member cannot invite admin (SECURITY FIX)', function () {
// Login as member
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
// Attempt to invite someone as admin
Livewire::test(InviteLink::class)
->set('email', 'newadmin@example.com')
->set('role', 'admin')
->call('viaLink')
->assertDispatched('error');
});
test('member cannot invite owner (SECURITY FIX)', function () {
// Login as member
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
// Attempt to invite someone as owner
Livewire::test(InviteLink::class)
->set('email', 'newowner@example.com')
->set('role', 'owner')
->call('viaLink')
->assertDispatched('error');
});
test('admin cannot invite owner', function () {
// Login as admin
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
// Attempt to invite someone as owner
Livewire::test(InviteLink::class)
->set('email', 'newowner@example.com')
->set('role', 'owner')
->call('viaLink')
->assertDispatched('error');
});
test('admin can invite member', function () {
// Login as admin
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
// Invite someone as member
Livewire::test(InviteLink::class)
->set('email', 'newmember@example.com')
->set('role', 'member')
->call('viaLink')
->assertDispatched('success');
// Verify invitation was created
$this->assertDatabaseHas('team_invitations', [
'email' => 'newmember@example.com',
'role' => 'member',
'team_id' => $this->team->id,
]);
});
test('admin can invite admin', function () {
// Login as admin
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
// Invite someone as admin
Livewire::test(InviteLink::class)
->set('email', 'newadmin@example.com')
->set('role', 'admin')
->call('viaLink')
->assertDispatched('success');
// Verify invitation was created
$this->assertDatabaseHas('team_invitations', [
'email' => 'newadmin@example.com',
'role' => 'admin',
'team_id' => $this->team->id,
]);
});
test('owner can invite member', function () {
// Login as owner
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
// Invite someone as member
Livewire::test(InviteLink::class)
->set('email', 'newmember@example.com')
->set('role', 'member')
->call('viaLink')
->assertDispatched('success');
// Verify invitation was created
$this->assertDatabaseHas('team_invitations', [
'email' => 'newmember@example.com',
'role' => 'member',
'team_id' => $this->team->id,
]);
});
test('owner can invite admin', function () {
// Login as owner
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
// Invite someone as admin
Livewire::test(InviteLink::class)
->set('email', 'newadmin@example.com')
->set('role', 'admin')
->call('viaLink')
->assertDispatched('success');
// Verify invitation was created
$this->assertDatabaseHas('team_invitations', [
'email' => 'newadmin@example.com',
'role' => 'admin',
'team_id' => $this->team->id,
]);
});
test('owner can invite owner', function () {
// Login as owner
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
// Invite someone as owner
Livewire::test(InviteLink::class)
->set('email', 'newowner@example.com')
->set('role', 'owner')
->call('viaLink')
->assertDispatched('success');
// Verify invitation was created
$this->assertDatabaseHas('team_invitations', [
'email' => 'newowner@example.com',
'role' => 'owner',
'team_id' => $this->team->id,
]);
});
test('member cannot bypass policy by calling viaEmail', function () {
// Login as member
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
// Attempt to invite someone as admin via email
Livewire::test(InviteLink::class)
->set('email', 'newadmin@example.com')
->set('role', 'admin')
->call('viaEmail')
->assertDispatched('error');
});
});

View file

@ -0,0 +1,184 @@
<?php
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create a team with owner, admin, and member
$this->team = Team::factory()->create();
$this->owner = User::factory()->create();
$this->admin = User::factory()->create();
$this->member = User::factory()->create();
$this->team->members()->attach($this->owner->id, ['role' => 'owner']);
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
$this->team->members()->attach($this->member->id, ['role' => 'member']);
});
describe('update permission', function () {
test('owner can update team', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('update', $this->team))->toBeTrue();
});
test('admin can update team', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('update', $this->team))->toBeTrue();
});
test('member cannot update team', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('update', $this->team))->toBeFalse();
});
test('non-team member cannot update team', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('update', $this->team))->toBeFalse();
});
});
describe('delete permission', function () {
test('owner can delete team', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('delete', $this->team))->toBeTrue();
});
test('admin can delete team', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('delete', $this->team))->toBeTrue();
});
test('member cannot delete team', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('delete', $this->team))->toBeFalse();
});
test('non-team member cannot delete team', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('delete', $this->team))->toBeFalse();
});
});
describe('manageMembers permission', function () {
test('owner can manage members', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('manageMembers', $this->team))->toBeTrue();
});
test('admin can manage members', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('manageMembers', $this->team))->toBeTrue();
});
test('member cannot manage members', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('manageMembers', $this->team))->toBeFalse();
});
test('non-team member cannot manage members', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('manageMembers', $this->team))->toBeFalse();
});
});
describe('viewAdmin permission', function () {
test('owner can view admin panel', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('viewAdmin', $this->team))->toBeTrue();
});
test('admin can view admin panel', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('viewAdmin', $this->team))->toBeTrue();
});
test('member cannot view admin panel', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('viewAdmin', $this->team))->toBeFalse();
});
test('non-team member cannot view admin panel', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('viewAdmin', $this->team))->toBeFalse();
});
});
describe('manageInvitations permission (privilege escalation fix)', function () {
test('owner can manage invitations', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('manageInvitations', $this->team))->toBeTrue();
});
test('admin can manage invitations', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('manageInvitations', $this->team))->toBeTrue();
});
test('member cannot manage invitations (SECURITY FIX)', function () {
// This test verifies the privilege escalation vulnerability is fixed
// Previously, members could see and manage admin invitations
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('manageInvitations', $this->team))->toBeFalse();
});
test('non-team member cannot manage invitations', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('manageInvitations', $this->team))->toBeFalse();
});
});
describe('view permission', function () {
test('owner can view team', function () {
$this->actingAs($this->owner);
session(['currentTeam' => $this->team]);
expect($this->owner->can('view', $this->team))->toBeTrue();
});
test('admin can view team', function () {
$this->actingAs($this->admin);
session(['currentTeam' => $this->team]);
expect($this->admin->can('view', $this->team))->toBeTrue();
});
test('member can view team', function () {
$this->actingAs($this->member);
session(['currentTeam' => $this->team]);
expect($this->member->can('view', $this->team))->toBeTrue();
});
test('non-team member cannot view team', function () {
$outsider = User::factory()->create();
$this->actingAs($outsider);
session(['currentTeam' => $this->team]);
expect($outsider->can('view', $this->team))->toBeFalse();
});
});