mirror of
https://github.com/coollabsio/coolify.git
synced 2025-12-28 05:34:50 +00:00
This commit addresses a critical security vulnerability where low-privileged
users (members) could invite high-privileged users (admins/owners) to teams,
allowing them to escalate their own privileges through password reset.
Root Causes Fixed:
1. TeamPolicy authorization checks were commented out, allowing all team
members to manage invitations instead of just admins/owners
2. Missing role elevation checks in InviteLink component allowed members
to invite users with higher privileges
Security Fixes:
1. app/Policies/TeamPolicy.php
- Uncommented and enforced authorization checks for:
* update() - Only admins/owners can update team settings
* delete() - Only admins/owners can delete teams
* manageMembers() - Only admins/owners can manage team members
* viewAdmin() - Only admins/owners can view admin panel
* manageInvitations() - Only admins/owners can manage invitations
2. app/Livewire/Team/InviteLink.php
- Added explicit role elevation checks to prevent:
* Members from inviting admins or owners
* Admins from inviting owners (defense-in-depth)
- Validates that inviter has sufficient privileges for target role
Test Coverage:
1. tests/Feature/TeamPolicyTest.php
- 24 comprehensive tests covering all policy methods
- Tests for owner, admin, member, and non-member access
- Specific tests for the privilege escalation vulnerability
2. tests/Feature/TeamInvitationPrivilegeEscalationTest.php
- 11 tests covering all role elevation scenarios
- Tests member → admin/owner escalation (blocked)
- Tests admin → owner escalation (blocked)
- Tests valid invitation paths for each role
Impact:
- Prevents privilege escalation attacks
- Protects all Coolify instances from unauthorized access
- Enforces proper role hierarchy in team management
References:
- Identified by Aikido AI whitebox pentest service
- CVE: Pending assignment
- Severity: Critical
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
123 lines
4.3 KiB
PHP
123 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Team;
|
|
|
|
use App\Models\TeamInvitation;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
|
use Illuminate\Notifications\Messages\MailMessage;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Component;
|
|
use Visus\Cuid2\Cuid2;
|
|
|
|
class InviteLink extends Component
|
|
{
|
|
use AuthorizesRequests;
|
|
|
|
public string $email;
|
|
|
|
public string $role = 'member';
|
|
|
|
protected $rules = [
|
|
'email' => 'required|email',
|
|
'role' => 'required|string',
|
|
];
|
|
|
|
public function mount()
|
|
{
|
|
$this->email = isDev() ? 'test3@example.com' : '';
|
|
}
|
|
|
|
public function viaEmail()
|
|
{
|
|
$this->generateInviteLink(sendEmail: true);
|
|
}
|
|
|
|
public function viaLink()
|
|
{
|
|
$this->generateInviteLink(sendEmail: false);
|
|
}
|
|
|
|
private function generateInviteLink(bool $sendEmail = false)
|
|
{
|
|
try {
|
|
$this->authorize('manageInvitations', currentTeam());
|
|
$this->validate();
|
|
|
|
// 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');
|
|
if ($member_emails->contains($this->email)) {
|
|
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
|
}
|
|
$uuid = new Cuid2(32);
|
|
$link = url('/').config('constants.invitation.link.base_url').$uuid;
|
|
$user = User::whereEmail($this->email)->first();
|
|
|
|
if (is_null($user)) {
|
|
$password = Str::password();
|
|
$user = User::create([
|
|
'name' => str($this->email)->before('@'),
|
|
'email' => $this->email,
|
|
'password' => Hash::make($password),
|
|
'force_password_reset' => true,
|
|
]);
|
|
$token = Crypt::encryptString("{$user->email}@@@$password");
|
|
$link = route('auth.link', ['token' => $token]);
|
|
}
|
|
$invitation = TeamInvitation::whereEmail($this->email)->first();
|
|
if (! is_null($invitation)) {
|
|
$invitationValid = $invitation->isValid();
|
|
if ($invitationValid) {
|
|
return handleError(livewire: $this, customErrorMessage: "Pending invitation already exists for $this->email.");
|
|
} else {
|
|
$invitation->delete();
|
|
}
|
|
}
|
|
|
|
$invitation = TeamInvitation::firstOrCreate([
|
|
'team_id' => currentTeam()->id,
|
|
'uuid' => $uuid,
|
|
'email' => $this->email,
|
|
'role' => $this->role,
|
|
'link' => $link,
|
|
'via' => $sendEmail ? 'email' : 'link',
|
|
]);
|
|
if ($sendEmail) {
|
|
$mail = new MailMessage;
|
|
$mail->view('emails.invitation-link', [
|
|
'team' => currentTeam()->name,
|
|
'invitation_link' => $link,
|
|
]);
|
|
$mail->subject('You have been invited to '.currentTeam()->name.' on '.config('app.name').'.');
|
|
send_user_an_email($mail, $this->email);
|
|
$this->dispatch('success', 'Invitation sent via email.');
|
|
$this->dispatch('refreshInvitations');
|
|
|
|
return;
|
|
} else {
|
|
$this->dispatch('success', 'Invitation link generated.');
|
|
$this->dispatch('refreshInvitations');
|
|
}
|
|
} catch (\Throwable $e) {
|
|
$error_message = $e->getMessage();
|
|
if ($e->getCode() === '23505') {
|
|
$error_message = 'Invitation already sent.';
|
|
}
|
|
|
|
return handleError(error: $e, livewire: $this, customErrorMessage: $error_message);
|
|
}
|
|
}
|
|
}
|