HardТеория3 min

Подвохи Security

Порядок firewalls, access_control first match, remember me, voter priority, password hasher migration

Подвох 1: Порядок Firewalls -- КРИТИЧЕН

Firewalls обрабатываются сверху вниз. Первый совпавший firewall обрабатывает запрос, остальные ИГНОРИРУЮТСЯ.

# security.yaml
security:
    firewalls:
        # ПОРЯДОК КРИТИЧЕН!

        # 1. dev firewall -- должен быть ПЕРВЫМ
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        # 2. API firewall
        api:
            pattern: ^/api
            stateless: true
            custom_authenticators:
                - App\Security\ApiTokenAuthenticator

        # 3. Main firewall -- ПОСЛЕДНИЙ, ловит всё остальное
        main:
            lazy: true
            form_login:
                login_path: login
                check_path: login
<?php

declare(strict_types=1);

// Подвох: URL /api/admin обрабатывается firewall "api", а НЕ "main"!
// Даже если в main есть form_login, /api/* уже перехвачен.

// Ещё подвох: если поменять порядок:
// main: (pattern: ^/) -- перехватит ВСЁ, включая /api!
// api: (pattern: ^/api) -- НИКОГДА не сработает!

На экзамене: Firewall с более широким pattern (^/) должен стоять ПОСЛЕДНИМ. Если он стоит первым -- все последующие firewalls будут мертвым кодом.

Подвох 2: access_control -- First Match Wins

access_control работает по принципу "первое совпадение побеждает". Более специфичные правила должны быть ВЫШЕ.

security:
    access_control:
        # НЕПРАВИЛЬНЫЙ ПОРЯДОК:
        - { path: ^/admin, roles: ROLE_ADMIN }
        - { path: ^/admin/super, roles: ROLE_SUPER_ADMIN }
        # ^/admin/super совпадёт с ПЕРВЫМ правилом (^/admin)
        # и потребует только ROLE_ADMIN вместо ROLE_SUPER_ADMIN!

        # ПРАВИЛЬНЫЙ ПОРЯДОК:
        - { path: ^/admin/super, roles: ROLE_SUPER_ADMIN }  # Специфичное ПЕРВЫМ
        - { path: ^/admin, roles: ROLE_ADMIN }                # Общее ПОСЛЕДНИМ

На экзамене: access_control использует first match. Более специфичные правила (с более длинным path) ДОЛЖНЫ быть выше общих. Это одна из самых частых ошибок в конфигурации безопасности.

Подвох 3: IS_AUTHENTICATED_FULLY vs IS_AUTHENTICATED_REMEMBERED

Это НЕ одно и то же, и разница критична для безопасности.

<?php

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

final class AccountController extends AbstractController
{
    // IS_AUTHENTICATED_FULLY -- пользователь ввёл логин/пароль В ЭТОЙ сессии
    #[Route('/account/change-password', name: 'change_password')]
    #[IsGranted('IS_AUTHENTICATED_FULLY')]
    public function changePassword(): Response
    {
        // Безопасно -- пользователь точно ввёл пароль
        return new Response('Change password form');
    }

    // IS_AUTHENTICATED_REMEMBERED -- пользователь аутентифицирован
    // через remember_me cookie (НЕ вводил пароль)
    #[Route('/dashboard', name: 'dashboard')]
    #[IsGranted('IS_AUTHENTICATED_REMEMBERED')]
    public function dashboard(): Response
    {
        // Может быть через remember_me -- менее безопасно
        return new Response('Dashboard');
    }
}

Иерархия аутентификации:

IS_AUTHENTICATED_FULLY
    |
    v (включает)
IS_AUTHENTICATED_REMEMBERED
    |
    v (включает)
IS_AUTHENTICATED
    |
    v
PUBLIC_ACCESS

На экзамене: IS_AUTHENTICATED_FULLY подразумевает IS_AUTHENTICATED_REMEMBERED, но НЕ наоборот! Для критичных операций (смена пароля, удаление аккаунта) ВСЕГДА используйте IS_AUTHENTICATED_FULLY.

Подвох 4: Remember Me Edge Cases

# security.yaml
security:
    firewalls:
        main:
            remember_me:
                secret: '%kernel.secret%'
                lifetime: 604800  # 1 неделя
                always_remember_me: false
                # Подвох: если always_remember_me: true,
                # checkbox "remember me" в форме ИГНОРИРУЕТСЯ!
<?php

declare(strict_types=1);

// Подвох 1: Remember me cookie НЕ обновляется при каждом запросе
// Он обновляется только при аутентификации через remember_me handler

// Подвох 2: При смене пароля remember_me cookie НЕ инвалидируется
// автоматически, если не реализована проверка через signature

// Подвох 3: remember_me НЕ работает с stateless firewalls
// stateless: true означает что сессия НЕ используется

На экзамене: always_remember_me: true делает чекбокс в форме бесполезным -- cookie создаётся всегда. Это частый подвох в вопросах о конфигурации.

Подвох 5: Voter Strategy и приоритет

AccessDecisionManager имеет три стратегии голосования:

# security.yaml
security:
    access_decision_manager:
        strategy: affirmative  # По умолчанию!
        # Варианты: affirmative, consensus, unanimous, priority
<?php

declare(strict_types=1);

// Стратегия AFFIRMATIVE (по умолчанию):
// Хотя бы один GRANT = доступ разрешён
// Voter1: GRANT, Voter2: DENY, Voter3: ABSTAIN -> GRANTED!

// Стратегия CONSENSUS:
// Большинство GRANT = доступ разрешён
// Voter1: GRANT, Voter2: DENY, Voter3: GRANT -> GRANTED (2 > 1)

// Стратегия UNANIMOUS:
// ВСЕ должны GRANT (ABSTAIN не считается)
// Voter1: GRANT, Voter2: DENY, Voter3: GRANT -> DENIED!

// Стратегия PRIORITY:
// Первый voter, который не ABSTAIN, решает
// Voter1: ABSTAIN, Voter2: DENY, Voter3: GRANT -> DENIED!

На экзамене: По умолчанию используется affirmative стратегия. Это значит, что ОДИН voter с GRANT перевешивает все DENY. Многие ожидают, что DENY перевешивает -- это ошибка!

Подвох 6: Password Hasher Migration

Symfony поддерживает автоматическую миграцию хешей паролей.

# security.yaml
security:
    password_hashers:
        App\Entity\User:
            algorithm: auto  # Использует лучший доступный алгоритм

        # Миграция: при логине старый хеш автоматически пересчитается
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
            algorithm: auto
            migrate_from:
                - bcrypt        # Старый алгоритм
                - sha256        # Ещё более старый
<?php

declare(strict_types=1);

namespace App\Entity;

use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;

final class User implements UserInterface, PasswordAuthenticatedUserInterface
{
    // Когда пользователь логинится с паролем, захешированным bcrypt,
    // Symfony АВТОМАТИЧЕСКИ:
    // 1. Проверяет пароль через bcrypt (из migrate_from)
    // 2. Пересчитывает хеш через текущий algorithm (auto = argon2id)
    // 3. Сохраняет новый хеш в БД
    // Но ТОЛЬКО если реализован PasswordUpgraderInterface в UserRepository!

    private ?string $password = null;

    public function getPassword(): ?string
    {
        return $this->password;
    }
}

На экзамене: Миграция паролей происходит прозрачно при логине. Но для сохранения нового хеша репозиторий ДОЛЖЕН реализовывать PasswordUpgraderInterface. Без него миграция не произойдёт.

Подвох 7: Firewall и путь логина

security:
    firewalls:
        api:
            pattern: ^/api
            stateless: true
        main:
            form_login:
                login_path: /login
                check_path: /login  # POST на /login
<?php

declare(strict_types=1);

// Подвох: login_path должен быть ДОСТУПЕН без аутентификации!
// Если access_control требует ROLE_USER для ^/ -- получите redirect loop:
// /login -> требует аутентификации -> redirect на /login -> ...

// ПРАВИЛЬНО:
// access_control:
//     - { path: ^/login$, roles: PUBLIC_ACCESS }
//     - { path: ^/, roles: ROLE_USER }

На экзамене: Путь логина (login_path) ВСЕГДА должен быть доступен без аутентификации (PUBLIC_ACCESS). Иначе получите бесконечный redirect.

Подвох 8: Иерархия ролей

security:
    role_hierarchy:
        ROLE_ADMIN: [ROLE_USER, ROLE_EDITOR]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
<?php

declare(strict_types=1);

// ROLE_SUPER_ADMIN автоматически имеет:
// ROLE_ADMIN, ROLE_USER, ROLE_EDITOR, ROLE_ALLOWED_TO_SWITCH

// Подвох: getRoles() возвращает только ЯВНЫЕ роли пользователя!
// Иерархия применяется ТОЛЬКО при проверке через isGranted()

$user->getRoles(); // ['ROLE_SUPER_ADMIN'] -- без унаследованных!
$this->isGranted('ROLE_USER'); // true -- иерархия работает

На экзамене: getRoles() возвращает только явно назначенные роли. Иерархия ролей применяется только через isGranted(), #[IsGranted] и access_control. Не путайте!

Проверь себя

5 из 7

Что вернёт метод $user->getRoles() для пользователя с ROLE_SUPER_ADMIN, если role_hierarchy: ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_USER]?

Пользователь аутентифицирован через remember_me cookie. Пройдёт ли проверка IS_AUTHENTICATED_FULLY?

Для автоматической миграции хешей паролей при логине, кроме настройки migrate_from, что ещё необходимо?

Стратегия voter по умолчанию -- affirmative. Три voter-а ответили: GRANT, DENY, DENY. Каков результат?

В access_control: первое правило ^/admin (ROLE_ADMIN), второе ^/admin/super (ROLE_SUPER_ADMIN). Какая роль нужна для /admin/super?