API который возвращает 200 с данными другого пользователя при подмене ID в URL: это баг broken object-level authorization, и его можно поймать Playwright API-тестом в процессе обычной разработки без пентестера. То же применимо к SQL-инъекции, XSS, брутфорсу аккаунтов и утечке чувствительных данных в API-ответах. Статья разбирает категории OWASP Top 10 наиболее тестируемые во время QA, с полными Playwright TypeScript-тестами для каждой из них которые напрямую подключаются к существующему CI-сьюту.

Почему QA должны тестировать безопасность

Security: ответственность каждого. У QA-инженеров есть преимущества.

  • Доступ к исходному коду и тестовым окружениям: у пентестеров часто нет
  • Понимание логики приложения: знаешь что должно и не должно быть возможным
  • Инфраструктура автотестов: security-проверки могут быть частью CI-пайплайна
  • Высокая частота тестирования: security-регрессии ловятся рано

OWASP Top 10 (наиболее релевантные для QA)

Open Web Application Security Project публикует список самых распространённых веб-уязвимостей. Вот те с которыми сталкиваешься чаще всего.

1. Нарушение контроля доступа

Пользователи получают доступ к ресурсам к которым не должны.

Тест:

test('regular user cannot access admin API', async ({ request }) => {
  // Вход как обычный участник
  const loginResp = await request.post('/api/auth/login', {
    data: { email: 'member@test.com', password: 'MemberPass1' },
  });
  const { token } = await loginResp.json();
  
  // Попытка получить доступ к admin-эндпоинту
  const response = await request.get('/api/admin/users', {
    headers: { Authorization: `Bearer ${token}` },
  });
  
  // Должно быть 403 Forbidden, не 200
  expect(response.status()).toBe(403);
});

test('user cannot view another user profile by changing ID', async ({ request }) => {
  const user1Token = await getToken('user1@test.com', 'Pass1');
  
  // Пользователь 1 пытается получить профиль Пользователя 2 по угаданному ID
  const response = await request.get('/api/users/999', {
    headers: { Authorization: `Bearer ${user1Token}` },
  });
  
  // Должно быть 403 или 404, не 200 с данными пользователя 2
  expect([403, 404]).toContain(response.status());
});

2. Проблемы аутентификации

test('account lockout after failed attempts', async ({ request }) => {
  // Неверный пароль 5 раз
  for (let i = 0; i < 5; i++) {
    await request.post('/api/auth/login', {
      data: { email: 'user@test.com', password: 'WrongPassword' },
    });
  }
  
  // 6-я попытка должна быть заблокирована
  const response = await request.post('/api/auth/login', {
    data: { email: 'user@test.com', password: 'WrongPassword' },
  });
  
  expect(response.status()).toBe(429);  // Too Many Requests
  const body = await response.json();
  expect(body.message).toMatch(/locked|too many/i);
});

test('password reset token expires', async ({ request }) => {
  // Запрос сброса
  await request.post('/api/auth/forgot-password', {
    data: { email: 'user@test.com' },
  });
  
  // В тестах используй истёкший токен из базы данных или мок
  const expiredToken = 'expired-reset-token-12345';
  
  const response = await request.post('/api/auth/reset-password', {
    data: { token: expiredToken, password: 'NewPass1!' },
  });
  
  expect(response.status()).toBe(400);
  const body = await response.json();
  expect(body.error).toMatch(/expired|invalid/i);
});

3. Инъекции: SQL-инъекция

test('login form is not SQL injectable', async ({ page }) => {
  await page.goto('/login');
  
  // Классический SQL-инъекционный payload
  await page.fill('[data-testid="email"]', "' OR '1'='1");
  await page.fill('[data-testid="password"]', "' OR '1'='1");
  await page.click('[data-testid="submit"]');
  
  // НЕ должен войти в систему
  await expect(page).not.toHaveURL('/dashboard');
  await expect(page.getByTestId('error-message')).toBeVisible();
});

test('search is not SQL injectable', async ({ request, authToken }) => {
  const injections = [
    "'; DROP TABLE users; --",
    "' UNION SELECT username, password FROM users--",
    "1=1",
    "' OR 1=1--",
  ];
  
  for (const payload of injections) {
    const response = await request.get(`/api/users?search=${encodeURIComponent(payload)}`, {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    
    // Должен вернуть 200 с пустыми/безопасными результатами, не ошибку или дамп данных
    expect(response.status()).toBe(200);
    const body = await response.json();
    
    // Не должен возвращать всех пользователей (означало бы что инъекция сработала)
    const count = Array.isArray(body.data) ? body.data.length : body.length;
    expect(count).toBeLessThan(100);  // Санитарная проверка — не дамп всех пользователей
  }
});

4. Межсайтовый скриптинг (XSS)

test('user input is not executed as script', async ({ page }) => {
  await page.goto('/login');
  
  // Входим
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');
  
  // Пытаемся внедрить скрипт в поле управляемое пользователем
  await page.goto('/profile');
  
  const xssPayload = '<script>alert("XSS")</script>';
  await page.fill('[data-testid="display-name"]', xssPayload);
  await page.click('[data-testid="save"]');
  
  // Уходим и возвращаемся чтобы проверить выполняется ли скрипт
  await page.goto('/dashboard');
  await page.goto('/profile');
  
  // Проверяем диалог (появится если XSS сработал)
  let dialogAppeared = false;
  page.on('dialog', dialog => {
    dialogAppeared = true;
    dialog.dismiss();
  });
  
  await page.waitForTimeout(1000);  // Даём время скрипту выполниться если он есть
  
  expect(dialogAppeared).toBe(false);
  
  // Имя должно отображаться как текст, а не выполняться
  const nameField = page.getByTestId('display-name');
  const value = await nameField.inputValue();
  expect(value).toBe(xssPayload);  // Сохранено как текст, не выполнено
});

5. Утечка чувствительных данных

test('password not returned in API response', async ({ request, authToken }) => {
  const response = await request.get('/api/users/1', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  
  const body = await response.json();
  
  expect(body.password).toBeUndefined();
  expect(body.passwordHash).toBeUndefined();
  expect(body.passwordSalt).toBeUndefined();
});

test('auth token not logged in response headers', async ({ request }) => {
  const response = await request.post('/api/auth/login', {
    data: { email: 'user@test.com', password: 'ValidPass1' },
  });
  
  // Токен должен быть в теле, а не раскрываться в заголовках
  const headers = response.headers();
  expect(headers['x-auth-token']).toBeUndefined();
  expect(headers['authorization']).toBeUndefined();
  
  // Но должен быть в теле
  const body = await response.json();
  expect(body.token).toBeDefined();
});

test('API uses HTTPS', async ({ request }) => {
  const response = await request.get('/api/users');
  
  // Проверяем HSTS-заголовок
  const headers = response.headers();
  expect(headers['strict-transport-security']).toBeDefined();
});

OWASP ZAP: автоматизированное security-сканирование

OWASP ZAP: бесплатный инструмент security-сканирования. Интегрируется с Playwright:

// Использование ZAP-прокси с Playwright
test.use({ proxy: { server: 'http://localhost:8080' } });  // ZAP proxy

test('security scan through ZAP', async ({ page }) => {
  await page.goto('/');
  await page.goto('/login');
  await page.goto('/products');
  // ZAP пассивно сканирует все запросы браузера
});

Или запускай ZAP программно:

# Docker ZAP сканирование
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://staging.myapp.com \
  -r zap-report.html

Проверка security-заголовков

test('security headers present', async ({ request }) => {
  const response = await request.get('/');
  const headers = response.headers();
  
  // Content Security Policy
  expect(headers['content-security-policy']).toBeDefined();
  
  // Защита от clickjacking
  expect(headers['x-frame-options']).toBeDefined();
  
  // Предотвращение MIME type sniffing
  expect(headers['x-content-type-options']).toBe('nosniff');
  
  // XSS-защита (устаревший, но полезный заголовок)
  expect(headers['x-xss-protection']).toBeDefined();
  
  // Только HTTPS
  expect(headers['strict-transport-security']).toBeDefined();
  
  // Не передавать реферер внешним сайтам
  expect(headers['referrer-policy']).toBeDefined();
});

Тестирование edge case авторизации

test.describe('IDOR (Insecure Direct Object Reference)', () => {
  test('user cannot delete another user\'s post', async ({ request }) => {
    // Токены для двух разных пользователей
    const user1Token = await getToken('user1@test.com', 'Pass1');
    const user2Token = await getToken('user2@test.com', 'Pass2');
    
    // Пользователь 2 создаёт пост
    const createResp = await request.post('/api/posts', {
      data: { title: 'User 2 Post', content: 'Content' },
      headers: { Authorization: `Bearer ${user2Token}` },
    });
    const post = await createResp.json();
    
    // Пользователь 1 пытается удалить пост Пользователя 2
    const deleteResp = await request.delete(`/api/posts/${post.id}`, {
      headers: { Authorization: `Bearer ${user1Token}` },
    });
    
    // Должно быть 403 Forbidden
    expect(deleteResp.status()).toBe(403);
    
    // Пост должен существовать
    const getResp = await request.get(`/api/posts/${post.id}`);
    expect(getResp.status()).toBe(200);
  });
});

Что QA должен тестировать по security

| Тест | Что проверяет |

|------|--------------|

| Контроль доступа | Пользователи не получают доступ к ресурсам выше своей роли |

| IDOR | Пользователи не получают ресурсы других по ID |

| Блокировка аккаунта | Защита от брутфорса |

| SQL-инъекция | Ввод пользователя не интерпретируется как SQL |

| XSS | Ввод пользователя не выполняется как скрипт |

| Утечка данных | Чувствительные поля не возвращаются в API |

| Security-заголовки | Присутствуют HTTPS, CSP, заголовки XSS-защиты |

Не нужно искать zero-day эксплойты. Найти и предотвратить уязвимости из OWASP Top 10 делает приложение значительно более защищённым и предотвращает самые распространённые атаки которые реально происходят.

→ See also: Основы тестирования безопасности, которые должен знать каждый QA-инженер | Авторизация в API-тестах: API-ключи, Bearer-токены, OAuth2, JWT | Что такое REST API? Практическое руководство для QA-инженеров