Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Питання на співбесіді з Безпека

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

103 питань

У багатоорендному (multi-tenant) застосунку дані різних клієнтів-компаній лежать в одних таблицях з колонкою tenant_id. Найгірший можливий інцидент - витік даних між орендарями, тож ізоляція має бути системною, а не «не забути where в кожному запиті».

Рівень застосунку - глобальна область видимості Eloquent:

#[ScopedBy(TenantScope::class)]
class Invoice extends Model {}

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where($model->qualifyColumn('tenant_id'), Tenant::current()->id);
    }
}

Кожен Eloquent-запит до моделі автоматично обмежено поточним орендарем. Плюс автоматичне заповнення tenant_id при створенні.

Слабкі місця глобальних областей:

  • не працюють поза Eloquent: DB::table('invoices'), сирі запити, звіти, деякі пакети;
  • withoutGlobalScopes() - один виклик знімає захист;
  • черги, команди, планувальник - немає HTTP-запиту, і «поточний орендар» має бути встановлений явно (інакше область або падає, або - гірше - не застосовується);
  • зв'язки й підзапити (whereHas, join) можуть обійти область для пов'язаних таблиць;
  • унікальні індекси й exists-валідація без tenant_id - перевірка «email уже зайнятий» по всіх орендарях розкриває існування даних.

Рівень бази - Row-Level Security (RLS) у PostgreSQL:

ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
    USING (tenant_id = current_setting('app.tenant_id')::bigint)
    WITH CHECK (tenant_id = current_setting('app.tenant_id')::bigint);
DB::statement("SELECT set_config('app.tenant_id', ?, true)", [$tenant->id]);   // true - лише в межах транзакції

Тепер будь-який запит - Eloquent, сирий SQL, звіт - бачить лише рядки поточного орендаря. Помилка в коді застосунку не перетвориться на витік.

Підводні камені RLS:

  • власник таблиці й суперкористувачі обходять RLS за замовчуванням - потрібен FORCE ROW LEVEL SECURITY і окрема роль застосунку без прав власника та без BYPASSRLS;
  • пули з'єднань (PgBouncer у режимі транзакцій, постійні з'єднання Octane): значення, встановлене для сесії, «перетікає» в наступний запит іншого орендаря. Тому - set_config(..., true) / SET LOCAL у транзакції на кожен запит;
  • міграції, адмінка й фонові задачі між орендарями потребують окремої ролі з явним обходом;
  • продуктивність: умова політики додається до кожного запиту - потрібні індекси з tenant_id першою колонкою.

Підсумок: глобальна область - зручність і перший рівень, RLS - гарантія на рівні бази. Для даних, де витік між клієнтами неприпустимий, - обидва, плюс тести «орендар A не бачить даних орендаря B» для кожної моделі.

Докладніше в документації: PostgreSQL: Row Security Policies

TOCTOU (time-of-check to time-of-use) - між перевіркою умови й використанням її результату стан змінюється. У вебзастосунку це паралельні запити, кожен з яких проходить перевірку до того, як інший запише зміни.

// вразливо
public function withdraw(Request $request)
{
    $account = $request->user()->account;

    if ($account->balance >= $request->amount) {        // перевірка
        $account->decrement('balance', $request->amount); // використання
        Payout::create([...]);
    }
}

Нападник відправляє 20 однакових запитів одночасно. Усі читають баланс 1000, усі проходять перевірку, і рахунок отримує 20 виплат. Інструменти на кшталт Burp Suite відправляють такі «пачки» з точністю до мілісекунд, тож це не теоретична атака.

Де це трапляється:

  • баланс, бонуси, подарункові картки, виведення коштів;
  • одноразові купони й промокоди («використати один раз»);
  • ліміти («до 3 безкоштовних проєктів», «один відгук на замовлення»);
  • голосування й лайки;
  • запрошення й одноразові посилання;
  • перевірка прав, після якої права відкликано, а дія вже виконується.

Як захищатися:

1. Атомарна умова в самому запиті до бази:

$updated = Account::whereKey($account->id)
    ->where('balance', '>=', $amount)
    ->decrement('balance', $amount);

if ($updated === 0) {
    throw new InsufficientFunds;
}

Перевірка й зміна - один оператор UPDATE ... WHERE balance >= ?. База гарантує, що паралельні запити не пройдуть обидва.

2. Блокування рядка в транзакції:

DB::transaction(function () use ($accountId, $amount) {
    $account = Account::lockForUpdate()->findOrFail($accountId);
    // поки транзакція не завершилась, інші lockForUpdate чекають
});

3. Унікальні обмеження - найнадійніше для «один раз»: унікальний індекс на (coupon_id, user_id) - другий запис просто не вставиться, хоч скільки паралельних запитів.

4. Атомарні блокування кешу - Cache::lock("payout:{$userId}", 10)->block(5, ...) для операцій, що зачіпають кілька систем.

5. Ключі ідемпотентності для операцій з грошима - повтор того самого запиту не створює другу операцію.

Чого недостатньо:

  • перевірка в застосунку без блокування - вікно гонитви лишається;
  • блокування в пам'яті процесу - на кількох серверах чи воркерах не працює;
  • «малоймовірно» - нападник спеціально робить це ймовірним.

Тестування: паралельні запити в тестах (Http::pool, Concurrency::run) або навантажувальні інструменти - звичайні послідовні тести таких помилок не бачать.

Докладніше в документації: PortSwigger: гонитва запитів

Помилки авторизації рідко ловлять «звичайні» тести: розробник перевіряє, що власник може виконати дію, а те, що чужий користувач не може, - не перевіряється. Тому авторизацію тестують окремо й системно.

1. Матриця доступу. Для кожного ресурсу - таблиця «роль × дія → очікуваний результат»:

гість власник інший користувач модератор адмін іншої організації
переглянути 404 200 404 200 404
змінити 401 200 403 403 404
видалити 401 200 403 200 404

Матриця - і документація, і основа тестів.

2. Датасети в Pest - один тест на всю матрицю:

dataset('post access', [
    'guest' => [fn () => null, 'update', 401],
    'owner' => [fn () => Post::first()->author, 'update', 200],
    'stranger' => [fn () => User::factory()->create(), 'update', 403],
]);

it('enforces access to posts', function (Closure $actor, string $action, int $status) {
    $post = Post::factory()->create();
    $user = $actor();

    $response = ($user ? $this->actingAs($user) : $this)
        ->putJson("/api/posts/{$post->id}", ['title' => 'Новий заголовок']);

    $response->assertStatus($status);

    if ($status !== 200) {
        expect($post->fresh()->title)->not->toBe('Новий заголовок');   // стан не змінився
    }
})->with('post access');

Перевіряти не лише статус, а й відсутність змін у базі: буває, що відповідь 403, а дія вже виконана.

3. Тест «кожен маршрут захищено». Пройтися по Route::getRoutes() і переконатися, що кожен маршрут має middleware auth чи явно позначений як публічний. Новий маршрут без авторизації ламає тест одразу:

it('protects every route', function () {
    $public = ['login', 'register', 'password.request', 'home'];

    collect(Route::getRoutes())
        ->reject(fn ($route) => in_array($route->getName(), $public, true))
        ->each(fn ($route) => expect($route->gatherMiddleware())->toContain('auth'));
});

4. Тести політик напряму - швидкі модульні тести для складних правил ($user->can('update', $post)), окремо від HTTP.

5. Багатоорендність: для кожної моделі - тест «орендар A не бачить даних орендаря B» на всіх шляхах: список, деталі, пошук, експорт, фільтри.

6. Ручне й автоматизоване тестування на продакшен-подібному середовищі: два облікові записи в браузері (чи розширення на кшталт Autorize для Burp), що повторюють запити одного користувача від імені іншого.

Процес: новий ендпойнт без тестів на заборону не проходить рев'ю коду - це найдешевший спосіб не допустити IDOR.

Докладніше в документації: OWASP Web Security Testing Guide

Звичайні логи застосунку відповідають на питання «що зламалося». Журнал аудиту відповідає на інші: хто, що, коли й до чого отримав доступ чи змінив. Без нього після інциденту неможливо з'ясувати масштаб витоку, а в разі спору - довести, хто виконав дію.

Які події записувати:

  • автентифікація: успішні й невдалі входи, вихід, зміна пароля, увімкнення/вимкнення 2FA, скидання пароля;
  • зміна прав: призначення ролей, запрошення в команду, видача й відкликання токенів;
  • доступ до чутливих даних: перегляд персональних даних, експорт, завантаження документів;
  • адміністративні дії: «увійти як користувач», зміна налаштувань безпеки, видалення даних;
  • відмови в доступі (403) - серія відмов від одного користувача часто означає спробу перебору ідентифікаторів.

Структура запису:

{
  "at": "2026-10-04T10:15:00Z",
  "actor": {"type": "user", "id": 42, "impersonated_by": null},
  "action": "invoice.downloaded",
  "subject": {"type": "invoice", "id": 1041},
  "tenant_id": 7,
  "ip": "203.0.113.5",
  "user_agent": "...",
  "request_id": "9f2c...",
  "result": "allowed"
}

Вимоги до журналу аудиту:

  • незмінність: запис лише додається; адміністратори застосунку не можуть його змінити чи видалити. Окреме сховище чи таблиця з правами лише на вставку, експорт у зовнішню систему (SIEM);
  • без секретів: паролі, токени, повні номери карток - ніколи;
  • мінімізація персональних даних: записувати ідентифікатори, а не вміст документів; врахувати вимоги GDPR щодо зберігання й видалення;
  • узгоджений час (UTC) і ідентифікатор запиту, щоб зв'язати з рештою логів;
  • термін зберігання відповідно до регуляторних вимог.

У Laravel:

  • spatie/laravel-activitylog - журнал змін моделей і власних подій;
  • події автентифікації (Login, Failed, Logout, PasswordReset) - слухачі, що пишуть у журнал;
  • Gate::after - зручне місце, щоб фіксувати рішення авторизації для чутливих дій.

Журнал без моніторингу допомагає лише після інциденту. Сповіщення на аномалії - масовий експорт, вхід адміністратора з нової країни, сотні відмов у доступі за хвилину - дають змогу помітити атаку, поки вона триває.

Докладніше в документації: OWASP: журналювання

WAF (Web Application Firewall) - фільтр HTTP-запитів перед застосунком. Хмарні WAF (Cloudflare, AWS WAF) працюють на краю мережі, ще до вашого сервера.

Що він добре робить:

  • поглинає DDoS-атаки мережевого рівня й об'ємні HTTP-флуди - ваш сервер їх не бачить;
  • блокує відомі шаблони атак (керовані правила для SQL-ін'єкцій, XSS, відомих CVE популярного ПЗ) - захист «на час», поки вразливість не виправлена в коді;
  • відсікає ботів і сканери: запити до /.env, /wp-admin, /phpmyadmin, підозрілі User-Agent, перевірки на кшталт Managed Challenge;
  • обмеження частоти на рівні краю - для логіну, пошуку, API;
  • геоблокування, списки IP, правила за ASN.

Чого WAF не робить:

  • не виправляє вразливостей у логіці застосунку: BOLA (чужий id в URL), відсутня авторизація, бізнес-логіка - для WAF це звичайні коректні запити;
  • не бачить усього: зашифровані дані в тілі, нестандартні формати, атаки, розтягнуті в часі;
  • обходиться кодуванням і варіаціями payload-ів - сигнатурний захист не буває повним.

WAF - додатковий шар, а не заміна безпечному коду.

Головна пастка - відкритий origin. Якщо IP-адреса сервера відома (історичні DNS-записи, піддомен без проксі, заголовки листів, сертифікати в журналах Certificate Transparency), зловмисник іде напряму на сервер, оминаючи WAF і захист від DDoS.

Як закрити origin:

  • фаєрвол приймає 80/443 лише з діапазонів IP Cloudflare (список публікується й змінюється - його треба оновлювати);
  • Authenticated Origin Pulls (mTLS між Cloudflare і сервером) - сервер перевіряє клієнтський сертифікат Cloudflare;
  • Cloudflare Tunnel - сервер узагалі не має відкритих вхідних портів, з'єднання ініціює агент з сервера;
  • нова IP-адреса після підключення проксі, якщо стара вже «засвітилася»;
  • усі піддомени, що ведуть на той самий сервер, - через проксі.

Справжній IP клієнта: за проксі Laravel бачить IP Cloudflare. Потрібно довіряти заголовкам лише від проксі (trustProxies з діапазонами Cloudflare) і брати CF-Connecting-IP - інакше ліміти частоти й журнали працюватимуть з неправильними адресами.

Докладніше в документації: Cloudflare: захист origin-сервера

Класичний підхід: у секретах CI лежать статичні ключі хмарного облікового запису (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) з правами на деплой.

Проблеми статичних ключів:

  • живуть роками - їх рідко ротують, бо це болісно;
  • витікають: у журналах збирання, через скомпрометовану залежність чи action у пайплайні, через неуважний echo, через форк з доступом до секретів;
  • працюють звідусіль - вкрадений ключ можна використати з будь-якого комп'ютера;
  • часто мають завеликі права («адміністратор, щоб точно запрацювало»).

OIDC (OpenID Connect) - без збережених секретів:

  1. на кожен запуск пайплайну CI-платформа (GitHub Actions, GitLab CI) видає короткоживучий підписаний токен з даними про запуск: репозиторій, гілка, середовище, workflow;
  2. хмарний провайдер (AWS, GCP, Azure) довіряє CI-платформі як провайдеру ідентичності й обмінює цей токен на тимчасові облікові дані на хвилини;
  3. довіра обмежена умовами: «лише репозиторій org/shop, лише гілка main, лише оточення production».
permissions:
  id-token: write   # дозволити запуску отримати OIDC-токен
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/deploy-shop
      aws-region: eu-central-1

Що це дає:

  • нема чого красти - у секретах CI немає довгоживучих ключів;
  • облікові дані живуть хвилини і прив'язані до конкретного запуску;
  • точні умови: пул-реквест з форку чи інша гілка роль не отримають.

Що важливо налаштувати правильно:

  • умови довіри (claims) мають бути вузькими - лише repo без гілки чи середовища дозволить будь-якій гілці (зокрема з необережного PR) деплоїти в продакшен;
  • права ролі - найменші: деплой конкретного сервісу, а не адміністратор облікового запису;
  • захист оточень у CI (обов'язкове затвердження для production).

Решта гігієни CI: закріплення сторонніх actions за хешем коміту (а не тегом), мінімальні permissions для GITHUB_TOKEN, обережність з pull_request_target, секрети не потрапляють у журнали.

Докладніше в документації: GitHub Actions: OpenID Connect

У хмарі застосунок отримує права не через ключі в .env, а через роль, прикріплену до сервера чи контейнера (IAM-роль інстансу, workload identity). Облікові дані видає сервіс метаданих за внутрішньою адресою 169.254.169.254.

Чому це зручно: немає статичних ключів, тимчасові облікові дані оновлюються автоматично.

Чому це небезпечно без захисту: будь-яка вразливість, що дозволяє змусити сервер зробити HTTP-запит (SSRF - «завантажити зображення за URL», генерація PDF з HTML, вебхук на адресу користувача), дає зловмиснику доступ до метаданих:

http://169.254.169.254/latest/meta-data/iam/security-credentials/app-role

і облікових даних ролі. Кілька відомих масштабних витоків даних почалися саме так.

Захист сервісу метаданих:

  • IMDSv2 обов'язковий (AWS): спершу PUT-запит за сесійним токеном, потім запити з заголовком токена. Типова SSRF дозволяє лише GET без власних заголовків - і токен отримати не може. Для нових інстансів IMDSv2 варто вимагати явно (HttpTokens=required);
  • hop limit = 1 - відповідь сервісу метаданих не проходить через додатковий мережевий «стрибок»: контейнер без host-мережі не дістане метадані хоста;
  • у Kubernetes - IRSA/workload identity замість ролі вузла: кожен под отримує власну роль.

Принцип найменших прав для ролі:

  • лише потрібні дії й ресурси: s3:PutObject і s3:GetObject на arn:aws:s3:::shop-uploads/*, а не s3:* на *;
  • окремі ролі для веб-застосунку, воркерів черг, задач бекапу - компрометація одного не відкриває решту;
  • жодних прав на IAM (створення користувачів і ролей) у застосунку;
  • умови: обмеження за VPC, тегами, мережею;
  • регулярний аудит невикористаних прав (IAM Access Analyzer та аналоги).

Захист від SSRF у коді лишається обов'язковим: перевірка URL і IP після розв'язання DNS, заборона приватних діапазонів і 169.254.0.0/16, вимкнені редиректи.

Виявлення: журнали хмарного провайдера (CloudTrail) з підозрілими діями від імені ролі застосунку (наприклад, виклики з невідомих IP) - сигнал до негайного реагування.

Докладніше в документації: AWS: налаштування сервісу метаданих (IMDS)

Ротація секретів потрібна регулярно (обмежити шкоду від непоміченого витоку) і негайно після підозри на компрометацію. Складність - не зламати роботу під час заміни.

Загальний принцип - період, коли діють обидва ключі:

  1. створити новий секрет;
  2. налаштувати систему приймати і старий, і новий;
  3. перевести всіх, хто використовує секрет, на новий;
  4. відкликати старий.

Так працює ротація ключів API партнерів, паролів до бази (два користувачі чи дві паролі в PostgreSQL), підписів вебхуків.

APP_KEY у Laravel шифрує cookie (зокрема сесійну), зашифровані атрибути моделей (каст encrypted), дані Crypt::encrypt(). Проста заміна ключа:

  • розлогінить усіх користувачів - старі cookie неможливо розшифрувати;
  • зламає зашифровані в базі дані - їх більше неможливо прочитати.

М'яка ротація через APP_PREVIOUS_KEYS:

APP_KEY="base64:новий..."
APP_PREVIOUS_KEYS="base64:старий..."
  • шифрування - завжди новим ключем;
  • розшифрування - спершу новим, потім по черзі попередніми.

Користувачі лишаються в системі, а сесії поступово перешифровуються новим ключем.

Але дані в базі самі не перешифруються. Значення, зашифровані старим ключем, лишаються такими, доки їх не перезапишуть. Перед тим як прибрати старий ключ із APP_PREVIOUS_KEYS, потрібна міграція даних: прочитати й зберегти кожне зашифроване значення (команда, що проходить по моделях частинами). Інакше видалення старого ключа - знову втрата даних.

Що ще залежить від APP_KEY: підписані URL (URL::signedRoute) - старі посилання стануть недійсними, якщо підпис не перевіряється попередніми ключами; знімки стану Livewire підписані ключем застосунку - відкриті сторінки отримають помилку при наступній дії.

Організаційно:

  • менеджер секретів (Vault, AWS Secrets Manager, Doppler, хмарні сховища) - центральне місце, журнал доступу, автоматична ротація для баз даних;
  • перелік, де використовується кожен секрет - без нього ротація перетворюється на пошук, що зламалося;
  • регламент реагування: хто і як ротує секрети при витоку, скільки часу це займає - відпрацьоване заздалегідь, а не вперше під час інциденту.

Докладніше в документації: Laravel: м'яка ротація ключів шифрування

Три способи знаходити вразливості руками людей - з різними цілями, вартістю й зрілістю, яку вони вимагають від команди.

Пентест (тестування на проникнення):

  • замовлена перевірка конкретної системи командою спеціалістів у визначені терміни й межі (scope);
  • результат - звіт з вразливостями, їх серйозністю й рекомендаціями;
  • плюси: глибина, перевірка бізнес-логіки, передбачувана вартість, звіт для аудиторів і клієнтів (часто вимога стандартів і корпоративних замовників);
  • мінуси: знімок на конкретний момент - наступний реліз може додати нові вразливості; результат залежить від кваліфікації конкретної команди.

VDP (Vulnerability Disclosure Policy, політика розкриття вразливостей):

  • публічна політика: як повідомити про вразливість, що дозволено досліджувати, що ні, як ви реагуєте і що не будете переслідувати дослідників, які діяли добросовісно;
  • без грошової винагороди (подяка, згадка в зали слави);
  • мінімальна вимога для будь-якої організації - разом із security.txt. Без неї люди, що випадково знайшли проблему, не знають, куди писати, і бояться юридичних наслідків.

Bug bounty:

  • VDP з грошовими винагородами залежно від серйозності, зазвичай через платформи (HackerOne, Bugcrowd, Intigriti);
  • безперервна перевірка багатьма дослідниками з різними навичками;
  • плюси: платите за результат, покриття нових релізів;
  • мінуси: потік звітів, частина з яких - дублікати й низькоякісні знахідки; потрібна команда, що швидко розбирає звіти й виправляє проблеми. Запуск bug bounty без зрілих процесів обертається завалом звітів і поганою репутацією серед дослідників.

Рекомендована послідовність зрілості:

  1. базова гігієна й автоматизовані перевірки (SAST, SCA, DAST);
  2. VDP і security.txt - канал для повідомлень;
  3. пентест перед важливими релізами й регулярно (раз на рік чи після великих змін);
  4. приватний bug bounty (запрошені дослідники), потім публічний - коли процес обробки налагоджено.

Що важливо в будь-якому варіанті:

  • чіткі межі: що можна тестувати (staging чи продакшен), що заборонено (DoS, соціальна інженерія, доступ до даних реальних користувачів);
  • терміни реакції: підтвердження отримання, оцінка, виправлення, повідомлення досліднику;
  • координоване розкриття: публікація подробиць - після виправлення, за домовленістю з дослідником.

Докладніше в документації: OWASP: розкриття вразливостей

Інцидент - не час вигадувати процес. Порядок дій, ролі й контакти мають бути визначені заздалегідь, інакше паніка коштує годин і доказів. NIST SP 800-61 Rev. 3 (2025) пов'язує реагування на інциденти із загальним керуванням кібербезпекою (CSF 2.0): підготовка, виявлення, реагування, відновлення - як безперервний цикл.

Основні кроки:

1. Виявлення й оцінка. Сповіщення моніторингу, повідомлення клієнта чи дослідника. Перше - зрозуміти масштаб: що скомпрометовано, з якого часу, чи триває атака.

2. Стримування (containment):

  • закрити вектор атаки (вимкнути вразливу функцію, правило WAF, заблокувати обліковий запис);
  • ротувати скомпрометовані секрети - ключі API, паролі до бази, APP_KEY, токени, сесії користувачів;
  • ізолювати уражені сервери, не знищуючи їх - вони потрібні для розслідування.

3. Збереження доказів: журнали, знімки дисків і пам'яті, копії підозрілих файлів - до того, як їх перезапише ротація журналів чи перевстановлення сервера. Записувати хронологію всіх дій.

4. Усунення й відновлення: виправлення вразливості, перевстановлення скомпрометованих систем з чистих образів, відновлення даних з бекапів, створених до компрометації, посилений моніторинг після повернення.

5. Повідомлення:

  • наглядовому органу - GDPR вимагає повідомити протягом 72 годин після того, як стало відомо про витік персональних даних, якщо він становить ризик для людей;
  • постраждалим людям - якщо ризик високий: що сталося, які дані, що вони можуть зробити (змінити пароль, стежити за шахрайськими листами);
  • клієнтам і партнерам - за умовами договорів;
  • юридичний відділ і, за потреби, правоохоронні органи.

6. Розбір після інциденту (post-mortem) без пошуку винних: як потрапили, чому не помітили раніше, що змінити в процесах і моніторингу. Задачі - в трекер з відповідальними.

Що підготувати заздалегідь:

  • план реагування з ролями (хто координує, хто комунікує, хто технічно розслідує) і контактами, доступний офлайн;
  • перелік секретів і процедури їх ротації;
  • журнали з достатнім терміном зберігання - без них неможливо встановити масштаб;
  • навчання - розіграш сценарію «витік бази» раз на рік показує прогалини краще за будь-який документ.

Помилки, яких варто уникати: приховувати інцидент, «тихо виправити» без розслідування масштабу, повідомляти неперевірену інформацію, знищити сервер разом із доказами.

Докладніше в документації: NIST SP 800-61 Rev. 3: реагування на інциденти

Нова категорія OWASP Top 10:2025 об'єднує вразливості, що виникають, коли система неправильно поводиться в нештатній ситуації: помилка, тайм-аут, недоступний сервіс, некоректні дані, вичерпані ресурси.

1. «Відкриття при помилці» (fail-open) - перевірка, що при збої пропускає:

function isAllowed(User $user, string $action): bool
{
    try {
        return $this->permissionService->check($user, $action);
    } catch (Throwable) {
        return true;   // «щоб не блокувати користувачів» - а насправді вимкнений контроль доступу
    }
}

Сервіс прав недоступний - і всі отримують доступ. Правильно - закриття при помилці (fail-closed): відмова й запис у журнал.

Те саме з перевіркою підпису вебхука, ліцензії, ліміту запитів, антифроду: виняток не повинен означати «перевірку пройдено».

2. Незавершені операції й неузгоджений стан. Гроші списано, а замовлення не створено; товар зарезервовано, а оплата впала. Транзакції бази, ідемпотентні операції, компенсуючі дії й узгодження (reconciliation) - щоб помилка посередині не лишала систему в стані, який можна використати.

3. Розкриття інформації через помилки: стеки викликів, SQL-запити, шляхи файлів, внутрішні адреси в повідомленнях про помилки. Користувачу - загальне повідомлення з ідентифікатором, деталі - в журнал.

4. Непередбачені стани як вектор атаки:

  • некоректні дані провокують виняток у місці, після якого пропущено перевірку;
  • різні повідомлення про помилки для «користувача не існує» і «невірний пароль» - перелік облікових записів;
  • null замість об'єкта, що трактується як «немає обмежень».

5. Виснаження ресурсів: необмежені розміри запитів, файлів, масивів, глибина рекурсії, кількість повторів - атака на доступність через «легальні», але завеликі дані.

Як захищатися:

  • закривати при помилці в усіх перевірках безпеки;
  • обробляти винятки там, де відомо, що робити, а не ковтати їх широкими catch (Throwable);
  • транзакції для операцій, що мають виконатися разом;
  • обмеження на розміри, кількості, час виконання;
  • тести негативних сценаріїв: що станеться, якщо сервіс прав, платіжний шлюз чи Redis недоступні, - і чи безпечний результат;
  • моніторинг помилок - сплеск винятків часто означає, що хтось шукає слабке місце.

Докладніше в документації: OWASP A10:2025

Перевірка безпеки «наприкінці» (пентест перед релізом) знаходить проблеми, коли їх найдорожче виправляти, і перетворює безпеку на перешкоду. Ідея shift left - вбудувати безпеку в кожен етап.

1. Вимоги й дизайн:

  • вимоги безпеки в задачах: «доступ лише власнику», «ліміт 5 спроб на хвилину», «журналювати зміну ролі» - як звичайні критерії приймання;
  • OWASP ASVS (Application Security Verification Standard, актуальна версія 5.0) - перелік перевірних вимог за рівнями: L1 для більшості застосунків, L2 для застосунків з чутливими даними, L3 для критичних систем. Зручно брати як джерело критеріїв, а не вигадувати їх щоразу;
  • моделювання загроз для функцій, що зачіпають автентифікацію, платежі, файли, персональні дані.

2. Розробка:

  • безпечні значення за замовчуванням у фреймворку й шаблонах проєкту (Laravel вже дає більшість: екранування, CSRF, підготовлені запити, хешування паролів);
  • спільні компоненти для типових задач (завантаження файлів, вебхуки, HTTP-клієнт із захистом від SSRF) - щоб кожен розробник не писав своє;
  • тести на безпеку: авторизація кожного ендпойнта, валідація, межі.

3. Перевірка в CI: SAST, аудит залежностей, сканування секретів, сканування образів - автоматично на кожен PR, з блокуванням критичних знахідок.

4. Definition of Done з пунктами безпеки: перевірено права, немає секретів у коді, нові залежності перевірено, події безпеки журналюються, документація оновлена.

5. Експлуатація: моніторинг і сповіщення, регулярні оновлення, DAST проти staging, VDP і security.txt, план реагування на інциденти.

6. Люди:

  • security champions - по одному розробнику в команді, що глибше розбирається в безпеці, стежить за практиками й є першою точкою для питань. Масштабується краще, ніж окрема команда безпеки, через яку мають проходити всі рішення;
  • навчання на реальних прикладах з власного коду й інцидентів, а не абстрактні курси;
  • культура без пошуку винних: про знайдену вразливість мають повідомляти, а не приховувати.

Як міряти: час від повідомлення про вразливість до виправлення, кількість критичних знахідок на пентестах (має зменшуватися), частка ендпойнтів з тестами авторизації, вік найстарішої необробленої вразливості в залежностях.

OWASP SAMM - модель зрілості, щоб оцінити поточний стан процесів і спланувати поступові покращення, а не все одразу.

Докладніше в документації: OWASP ASVS

Найдорожчі атаки часто не використовують жодної технічної вразливості: зловмисник просто викликає легітимну функцію у масштабі, на який бізнес не розраховував.

Типові сценарії:

  • SMS-накрутка (SMS pumping, toll fraud): бот запитує коди підтвердження на тисячі номерів у дорогих напрямках. Оператор, з яким у змові зловмисник, ділиться з ним виручкою, а застосунок платить за кожне SMS - рахунок на тисячі доларів за ніч;
  • промокоди й пробні періоди: тисячі реєстрацій для одноразової знижки чи безкоштовного тарифу;
  • реферальні бонуси: ферми акаунтів запрошують одна одну;
  • скупка дефіцитного товару ботами, автоматичне бронювання без оплати;
  • збір даних: перебір публічних профілів чи цін конкурентами.

Чому звичайне обмеження частоти не допомагає: ліміт «5 запитів на хвилину з IP» обходиться ротацією тисяч IP з резидентних проксі, а ліміт на акаунт - створенням нових акаунтів.

Багаторівневий захист:

Рівень Що обмежує
IP і підмережа грубі атаки з одного джерела
акаунт дії одного користувача
ціль дії кількість SMS на номер, на префікс країни, на напрямок
пристрій чи сесія відбиток браузера, токен застосунку
глобальний бюджет загальна кількість SMS на годину для всього сервісу
RateLimiter::for('sms', function (Request $request) {
    $phone = (string) $request->input('phone');

    return [
        Limit::perHour(3)->by('phone:' . $phone),
        Limit::perHour(10)->by('ip:' . $request->ip()),
        Limit::perHour(500)->by('sms:global'),
    ];
});

Що ще працює:

  • дозволені країни для SMS - лише ті, де справді є користувачі; дорогі міжнародні напрямки вимкнені за замовчуванням;
  • бюджети й сповіщення: ліміт витрат у провайдера SMS, алерт при стрибку кількості відправлень;
  • дешевша альтернатива SMS: вхід за посиланням на email, TOTP, passkeys;
  • тертя для ризикових дій: CAPTCHA чи Turnstile лише після підозрілих ознак, а не для всіх;
  • бізнес-правила: промокод прив'язаний до верифікованої картки чи телефону, реферальний бонус - після першої оплати, а не реєстрації;
  • затримка винагороди: бонус нараховується через кілька днів, коли шахрайство встигли виявити.

Моніторинг важливіший за правила: метрики на кожну дорогу дію (SMS, бонуси, пробні періоди) з порогами аномалій. Правила зловмисники обходять, а різкий стрибок графіка видно одразу.

Моделювання загроз для нової функції має включати питання «що буде, якщо цю дію викличуть мільйон разів» - разом з питаннями про доступ і валідацію.

Докладніше в документації: OWASP API6:2023 Unrestricted Access to Sensitive Business Flows

Питання з реальних технічних співбесід - 103 питання у 7 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.

Рівні
Junior 35 Middle 37 Senior 31

Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії