CVE-2023-2531 (PHP, published 2023-05-05T01:15Z)
Description: value: Improper Restriction of Excessive Authentication Attempts in GitHub repository azuracast/azuracast prior to 0.18.3.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-307 \u2014 Improper Restriction of Excessive Authentication Attempts
The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
Repository: https://github.com/azuracast/azuracast
Files changed:
CHANGELOG.md
frontend/vue/components/Admin/Settings.vue
frontend/vue/components/Admin/Settings/SecurityPrivacyTab.vue
src/Controller/Api/Admin/RelaysController.php
src/Controller/Api/Stations/Requests/SubmitAction.php
src/Entity/Enums/IpSources.php
src/Entity/Migration/Version20230428062001.php
src/Entity/Settings.php
src/Http/ServerRequest.php
src/RateLimit.php
Commit message: Tighten allowed IPs to avoid brute-force workarounds.


Vulnerable code:
                'analytics', 'always_use_ssl', 'api_access_control'
                class="col-md-12"
        private readonly Adapters $adapters
            $base_url = 'http://' . $request->getIp();
        private readonly StationRequestRepository $requestRepo
                $request->getIp(),
use RuntimeException;

    /**
     * Get the remote user's IP address as indicated by HTTP headers.
     */
    public function getIp(): string
    {
        $params = $this->serverRequest->getServerParams();

        $ip = $params['HTTP_CLIENT_IP']
            ?? $params['HTTP_X_FORWARDED_FOR']
            ?? $params['HTTP_X_FORWARDED']
            ?? $params['HTTP_FORWARDED_FOR']
            ?? $params['HTTP_FORWARDED']
            ?? $params['REMOTE_ADDR']
            ?? null;

        if (null === $ip) {
            throw new RuntimeException('No IP address attached to this request.');
        }

        // Handle the IP being separated by commas.
        $ipParts = explode(',', $ip);
        $ip = array_shift($ipParts);

        return trim($ip);
    }
        $ipKey = str_replace([':', '.'], '_', $request->getIp());

Fixed code:
- In sections of our application that depend on IP addresses, we've tightened our allowed IP addresses significantly to
  improve security and prevent brute-force flooding. If you're using a reverse proxy or CloudFlare, you should update
  your "IP Address Source" under the "System Settings" page.

        ip_source: {},

                'analytics', 'always_use_ssl', 'ip_source', 'api_access_control'
        ip_source: 'local',
            <b-wrapped-form-group
                id="edit_form_ip_source"
                class="col-md-6"
                :field="form.ip_source"
            >
                <template #label>
                    {{ $gettext('IP Address Source') }}
                </template>
                <template #description>
                    {{
                        $gettext('Customize this setting to ensure you get the correct IP address for remote users. Only change this setting if you use a reverse proxy, either within Docker or a third-party service like CloudFlare.')
                    }}
                </template>
                <template #default="slotProps">
                    <b-form-radio-group
                        :id="slotProps.id"
                        v-model="slotProps.field.$model"
                        stacked
                        :options="ipSourceOptions"
                    />
                </template>
            </b-wrapped-form-group>

                class="col-md-6"
import {useTranslate} from "~/vendor/gettext";
import {computed} from "vue";

const {$gettext} = useTranslate();

const ipSourceOptions = computed(() => {
    return [
        {
            value: 'local',
            text: $gettext('Local IP (Default)')
        },
        {
            value: 'cloudflare',
            text: $gettext('CloudFlare (CF-Connecting-IP)')
        },
        {
            value: 'xff',
            text: $gettext('Reverse Proxy (X-Forwarded-For)')
        }
    ]
});
        private readonly Adapters $adapters,
        private readonly Entity\Repository\SettingsRepository $settingsRepo
            $base_url = 'http://' . $this->settingsRepo->readSettings()->getIp($request);
use App\Entity\Repository\SettingsRepository;
        private readonly StationRequestRepository $requestRepo,
        private readonly SettingsRepository $settingsRepo
            $ip = $this->settingsRepo->readSettings()->getIp($request);

                $ip,
<?php

declare(strict_types=1);

namespace App\Entity\Enums;

use Psr\Http\Message\ServerRequestInterface;

enum IpSources: string
{
    case Local = 'local';
    case XForwardedFor = 'xff';
    case Cloudflare = 'cloudflare';

    public static function default(): self
    {
        return self::Local;
    }

    public function getIp(ServerRequestInterface $request): string
    {
        if (self::Cloudflare === $this) {
            $ip = $request->getHeaderLine('CF-Connecting-IP');
            if (!empty($ip)) {
                return $this->parseIp($ip);
            }
        }

        if (self::XForwardedFor === $this) {
            $ip = $request->getHeaderLine('X-Forwarded-For');
            if (!empty($ip)) {
                return $this->parseIp($ip);
            }
        }

        $serverParams = $request->getServerParams();
        $ip = $serverParams['REMOTE_ADDR'] ?? null;

        if (empty($ip)) {
            throw new \RuntimeException('No IP address attached to this request.');
        }

        return $this->parseIp($ip);
    }

    private function parseIp(string $ip): string
    {
        // Handle the IP being separated by commas.
        if (str_contains($ip, ',')) {
            $ipParts = explode(',', $ip);
            $ip = array_shift($ipParts);
        }

        return trim($ip);
    }
}
<?php

declare(strict_types=1);

namespace App\Entity\Migration;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

final class Version20230428062001 extends AbstractMigration
{
    public function getDescription(): string
    {
        return 'Add "IP Source" setting.';
    }

    public function up(Schema $schema): void
    {
        $this->addSql('ALTER TABLE settings ADD ip_source VARCHAR(50) DEFAULT NULL');
    }

    public function down(Schema $schema): void
    {
        $this->addSql('ALTER TABLE settings DROP ip_source');
    }
}
use Psr\Http\Message\ServerRequestInterface;
    #[
        OA\Property(description: "IP Address Source"),
        ORM\Column(length: 50, nullable: true),
        Groups(self::GROUP_GENERAL)
    ]
    protected ?string $ip_source = null;

    public function getIpSource(): string
    {
        return $this->ip_source ?? Entity\Enums\IpSources::default()->value;
    }

    public function getIpSourceEnum(): Entity\Enums\IpSources
    {
        return Entity\Enums\IpSources::tryFrom($this->ip_source ?? '') ?? Entity\Enums\IpSources::default();
    }

    public function getIp(ServerRequestInterface $request): string
    {
        return $this->getIpSourceEnum()->getIp($request);
    }

    public function setIpSource(?string $ipSource): void
    {
        if (null !== $ipSource && null === Entity\Enums\IpSources::tryFrom($ipSource)) {
            throw new InvalidArgumentException('Invalid IP source.');
        }

        $this->ip_source = $ipSource;
    }

use App\Entity\Repository\SettingsRepository;
        private readonly SettingsRepository $settingsRepo,
        $ip = $this->settingsRepo->readSettings()->getIp($request);

        $ipKey = str_replace([':', '.'], '_', $ip);