Питання на співбесіді з Безпека
Питання з реальних співбесід з відповідями: 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) або навантажувальні інструменти - звичайні послідовні тести таких помилок не бачать.
Помилки авторизації рідко ловлять «звичайні» тести: розробник перевіряє, що власник може виконати дію, а те, що чужий користувач не може, - не перевіряється. Тому авторизацію тестують окремо й системно.
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.
Звичайні логи застосунку відповідають на питання «що зламалося». Журнал аудиту відповідає на інші: хто, що, коли й до чого отримав доступ чи змінив. Без нього після інциденту неможливо з'ясувати масштаб витоку, а в разі спору - довести, хто виконав дію.
Які події записувати:
- автентифікація: успішні й невдалі входи, вихід, зміна пароля, увімкнення/вимкнення 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- зручне місце, щоб фіксувати рішення авторизації для чутливих дій.
Журнал без моніторингу допомагає лише після інциденту. Сповіщення на аномалії - масовий експорт, вхід адміністратора з нової країни, сотні відмов у доступі за хвилину - дають змогу помітити атаку, поки вона триває.
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) - без збережених секретів:
- на кожен запуск пайплайну CI-платформа (GitHub Actions, GitLab CI) видає короткоживучий підписаний токен з даними про запуск: репозиторій, гілка, середовище, workflow;
- хмарний провайдер (AWS, GCP, Azure) довіряє CI-платформі як провайдеру ідентичності й обмінює цей токен на тимчасові облікові дані на хвилини;
- довіра обмежена умовами: «лише репозиторій
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, секрети не потрапляють у журнали.
У хмарі застосунок отримує права не через ключі в .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)
Ротація секретів потрібна регулярно (обмежити шкоду від непоміченого витоку) і негайно після підозри на компрометацію. Складність - не зламати роботу під час заміни.
Загальний принцип - період, коли діють обидва ключі:
- створити новий секрет;
- налаштувати систему приймати і старий, і новий;
- перевести всіх, хто використовує секрет, на новий;
- відкликати старий.
Так працює ротація ключів 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 без зрілих процесів обертається завалом звітів і поганою репутацією серед дослідників.
Рекомендована послідовність зрілості:
- базова гігієна й автоматизовані перевірки (SAST, SCA, DAST);
- VDP і security.txt - канал для повідомлень;
- пентест перед важливими релізами й регулярно (раз на рік чи після великих змін);
- приватний bug bounty (запрошені дослідники), потім публічний - коли процес обробки налагоджено.
Що важливо в будь-якому варіанті:
- чіткі межі: що можна тестувати (staging чи продакшен), що заборонено (DoS, соціальна інженерія, доступ до даних реальних користувачів);
- терміни реакції: підтвердження отримання, оцінка, виправлення, повідомлення досліднику;
- координоване розкриття: публікація подробиць - після виправлення, за домовленістю з дослідником.
Інцидент - не час вигадувати процес. Порядок дій, ролі й контакти мають бути визначені заздалегідь, інакше паніка коштує годин і доказів. 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 недоступні, - і чи безпечний результат;
- моніторинг помилок - сплеск винятків часто означає, що хтось шукає слабке місце.
Перевірка безпеки «наприкінці» (пентест перед релізом) знаходить проблеми, коли їх найдорожче виправляти, і перетворює безпеку на перешкоду. Ідея 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 - модель зрілості, щоб оцінити поточний стан процесів і спланувати поступові покращення, а не все одразу.
Найдорожчі атаки часто не використовують жодної технічної вразливості: зловмисник просто викликає легітимну функцію у масштабі, на який бізнес не розраховував.
Типові сценарії:
- 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 темах, розібраних із відповідями. Нижче - розбивка за рівнями та темами, якщо хочете звузити підготовку.
Готуєтесь до співбесіди не просто так: зараз на сайті 145 відкритих вакансій Laravel і PHP. Переглянути вакансії