feat(api): enhance IP access control in middleware and settings; support CIDR notation and special case for 0.0.0.0 to allow all IPs
This commit is contained in:
@@ -18,12 +18,18 @@ class ApiAllowed
|
|||||||
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
|
return response()->json(['success' => true, 'message' => 'API is disabled.'], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! isDev()) {
|
|
||||||
if ($settings->allowed_ips) {
|
if ($settings->allowed_ips) {
|
||||||
$allowedIps = explode(',', $settings->allowed_ips);
|
// Check for special case: 0.0.0.0 means allow all
|
||||||
if (! in_array($request->ip(), $allowedIps)) {
|
if (trim($settings->allowed_ips) === '0.0.0.0') {
|
||||||
return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$allowedIps = explode(',', $settings->allowed_ips);
|
||||||
|
$allowedIps = array_map('trim', $allowedIps);
|
||||||
|
$allowedIps = array_filter($allowedIps); // Remove empty entries
|
||||||
|
|
||||||
|
if (! empty($allowedIps) && ! check_ip_against_allowlist($request->ip(), $allowedIps)) {
|
||||||
|
return response()->json(['success' => true, 'message' => 'You are not allowed to access the API.'], 403);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -4,6 +4,7 @@ namespace App\Livewire\Settings;
|
|||||||
|
|
||||||
use App\Models\InstanceSettings;
|
use App\Models\InstanceSettings;
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Rules\ValidIpOrCidr;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Hash;
|
use Hash;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
@@ -31,7 +32,6 @@ class Advanced extends Component
|
|||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $is_api_enabled;
|
public bool $is_api_enabled;
|
||||||
|
|
||||||
#[Validate('nullable|string')]
|
|
||||||
public ?string $allowed_ips = null;
|
public ?string $allowed_ips = null;
|
||||||
|
|
||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
@@ -40,6 +40,21 @@ class Advanced extends Component
|
|||||||
#[Validate('boolean')]
|
#[Validate('boolean')]
|
||||||
public bool $disable_two_step_confirmation;
|
public bool $disable_two_step_confirmation;
|
||||||
|
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'server' => 'required',
|
||||||
|
'is_registration_enabled' => 'boolean',
|
||||||
|
'do_not_track' => 'boolean',
|
||||||
|
'is_dns_validation_enabled' => 'boolean',
|
||||||
|
'custom_dns_servers' => 'nullable|string',
|
||||||
|
'is_api_enabled' => 'boolean',
|
||||||
|
'allowed_ips' => ['nullable', 'string', new ValidIpOrCidr],
|
||||||
|
'is_sponsorship_popup_enabled' => 'boolean',
|
||||||
|
'disable_two_step_confirmation' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
if (! isInstanceAdmin()) {
|
if (! isInstanceAdmin()) {
|
||||||
@@ -67,12 +82,76 @@ class Advanced extends Component
|
|||||||
return str($dns)->trim()->lower();
|
return str($dns)->trim()->lower();
|
||||||
})->unique()->implode(',');
|
})->unique()->implode(',');
|
||||||
|
|
||||||
|
// Handle allowed IPs with subnet support and 0.0.0.0 special case
|
||||||
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
|
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
|
||||||
$this->allowed_ips = str($this->allowed_ips)->trim()->explode(',')->map(function ($ip) {
|
|
||||||
return str($ip)->trim();
|
// Check if user entered 0.0.0.0 or left field empty (both allow access from anywhere)
|
||||||
})->unique()->implode(',');
|
$allowsFromAnywhere = false;
|
||||||
|
if (empty($this->allowed_ips)) {
|
||||||
|
$allowsFromAnywhere = true;
|
||||||
|
} elseif ($this->allowed_ips === '0.0.0.0' || str_contains($this->allowed_ips, '0.0.0.0')) {
|
||||||
|
$allowsFromAnywhere = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's 0.0.0.0 (allow all) or empty
|
||||||
|
if ($this->allowed_ips === '0.0.0.0' || empty($this->allowed_ips)) {
|
||||||
|
// Keep as is - empty means no restriction, 0.0.0.0 means allow all
|
||||||
|
} else {
|
||||||
|
// Validate and clean up the entries
|
||||||
|
$invalidEntries = [];
|
||||||
|
$validEntries = str($this->allowed_ips)->trim()->explode(',')->map(function ($entry) use (&$invalidEntries) {
|
||||||
|
$entry = str($entry)->trim()->toString();
|
||||||
|
|
||||||
|
if (empty($entry)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's valid CIDR notation
|
||||||
|
if (str_contains($entry, '/')) {
|
||||||
|
[$ip, $mask] = explode('/', $entry);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP) && is_numeric($mask) && $mask >= 0 && $mask <= 32) {
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
$invalidEntries[] = $entry;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a valid IP address
|
||||||
|
if (filter_var($entry, FILTER_VALIDATE_IP)) {
|
||||||
|
return $entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invalidEntries[] = $entry;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})->filter()->unique();
|
||||||
|
|
||||||
|
if (! empty($invalidEntries)) {
|
||||||
|
$this->dispatch('error', 'Invalid IP addresses or subnets: '.implode(', ', $invalidEntries));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check if we have no valid entries after filtering
|
||||||
|
if ($validEntries->isEmpty()) {
|
||||||
|
$this->dispatch('error', 'No valid IP addresses or subnets provided');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->allowed_ips = $validEntries->implode(',');
|
||||||
|
}
|
||||||
|
|
||||||
$this->instantSave();
|
$this->instantSave();
|
||||||
|
|
||||||
|
// Show security warning if allowing access from anywhere
|
||||||
|
if ($allowsFromAnywhere) {
|
||||||
|
$message = empty($this->allowed_ips)
|
||||||
|
? 'Empty IP allowlist allows API access from anywhere.<br><br>This is not recommended for production environments!'
|
||||||
|
: 'Using 0.0.0.0 allows API access from anywhere.<br><br>This is not recommended for production environments!';
|
||||||
|
$this->dispatch('warning', $message);
|
||||||
|
}
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
return handleError($e, $this);
|
return handleError($e, $this);
|
||||||
}
|
}
|
||||||
|
@@ -1025,6 +1025,64 @@ function ip_match($ip, $cidrs, &$match = null)
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function check_ip_against_allowlist($ip, $allowlist)
|
||||||
|
{
|
||||||
|
if (empty($allowlist)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((array) $allowlist as $allowed) {
|
||||||
|
$allowed = trim($allowed);
|
||||||
|
|
||||||
|
if (empty($allowed)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a CIDR notation
|
||||||
|
if (str_contains($allowed, '/')) {
|
||||||
|
[$subnet, $mask] = explode('/', $allowed);
|
||||||
|
|
||||||
|
// Special case: 0.0.0.0 with any subnet means allow all
|
||||||
|
if ($subnet === '0.0.0.0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask = (int) $mask;
|
||||||
|
|
||||||
|
// Validate mask
|
||||||
|
if ($mask < 0 || $mask > 32) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate network addresses
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$subnet_long = ip2long($subnet);
|
||||||
|
|
||||||
|
if ($ip_long === false || $subnet_long === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mask_long = ~((1 << (32 - $mask)) - 1);
|
||||||
|
|
||||||
|
if (($ip_long & $mask_long) == ($subnet_long & $mask_long)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Special case: 0.0.0.0 means allow all
|
||||||
|
if ($allowed === '0.0.0.0') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct IP comparison
|
||||||
|
if ($ip === $allowed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null)
|
function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = null, ?string $uuid = null)
|
||||||
{
|
{
|
||||||
if (is_null($teamId)) {
|
if (is_null($teamId)) {
|
||||||
|
Reference in New Issue
Block a user