Uma API que retorna 200 com dados de outro usuário quando você troca um ID na URL tem um bug de autorização quebrada no nível do objeto. Você consegue detectar isso com um teste de API no Playwright durante o desenvolvimento regular, sem precisar de um pentester. O mesmo vale para SQL injection, XSS, brute-force em contas e exposição de dados sensíveis em respostas de API.
Por que QA deve fazer testes de segurança
Segurança é responsabilidade de todos. Engenheiros de QA têm vantagens reais:
- Acesso ao código-fonte e aos ambientes de teste — pentesters frequentemente não têm
- Entendimento da lógica da aplicação — você sabe o que deveria e não deveria ser possível
- Infraestrutura de testes automatizados — verificações de segurança podem fazer parte do pipeline de CI
- Cadência de testes frequente — regressões de segurança são detectadas cedo
O OWASP Top 10 (mais relevante para QA)
O Open Web Application Security Project publica as vulnerabilidades web mais comuns. Estas são as que você vai encontrar com mais frequência.
1. Controle de acesso quebrado
Usuários acessando recursos que não deveriam poder acessar.
Teste:test('usuário comum não consegue acessar API de admin', async ({ request }) => {
// Login como membro comum
const loginResp = await request.post('/api/auth/login', {
data: { email: 'member@test.com', password: 'MemberPass1' },
});
const { token } = await loginResp.json();
// Tentar acessar endpoint de admin
const response = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${token}` },
});
// Deve ser 403 Forbidden, não 200
expect(response.status()).toBe(403);
});
test('usuário não consegue ver perfil de outro usuário trocando ID', async ({ request }) => {
const user1Token = await getToken('user1@test.com', 'Pass1');
// Usuário 1 tenta acessar perfil do Usuário 2 adivinhando o ID
const response = await request.get('/api/users/999', {
headers: { Authorization: `Bearer ${user1Token}` },
});
// Deve ser 403 ou 404, não 200 com os dados do usuário 2
expect([403, 404]).toContain(response.status());
});2. Problemas de autenticação
test('conta é bloqueada após tentativas falhas', async ({ request }) => {
// Tentar senha errada 5 vezes
for (let i = 0; i < 5; i++) {
await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'SenhaErrada' },
});
}
// 6ª tentativa deve ser bloqueada
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'SenhaErrada' },
});
expect(response.status()).toBe(429); // Too Many Requests
const body = await response.json();
expect(body.message).toMatch(/bloqueado|muitas tentativas/i);
});
test('token de reset de senha expira', async ({ request }) => {
// Solicitar reset
await request.post('/api/auth/forgot-password', {
data: { email: 'user@test.com' },
});
// Em testes, use um token expirado do banco de dados ou mock
const expiredToken = 'expired-reset-token-12345';
const response = await request.post('/api/auth/reset-password', {
data: { token: expiredToken, password: 'NovaSenha1!' },
});
expect(response.status()).toBe(400);
const body = await response.json();
expect(body.error).toMatch(/expirado|inválido/i);
});3. Injeção: SQL injection
test('formulário de login não é vulnerável a SQL injection', async ({ page }) => {
await page.goto('/login');
// Payload clássico de SQL injection
await page.fill('[data-testid="email"]', "' OR '1'='1");
await page.fill('[data-testid="password"]', "' OR '1'='1");
await page.click('[data-testid="submit"]');
// NÃO deve estar logado
await expect(page).not.toHaveURL('/dashboard');
await expect(page.getByTestId('error-message')).toBeVisible();
});
test('busca não é vulnerável a SQL injection', 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}` },
});
// Deve retornar 200 com resultados seguros/vazios, não erro ou dump de dados
expect(response.status()).toBe(200);
const body = await response.json();
// Não deve retornar todos os usuários (indicaria que a injeção funcionou)
const count = Array.isArray(body.data) ? body.data.length : body.length;
expect(count).toBeLessThan(100); // Verificação básica: não está despejando todos os usuários
}
});4. Cross-Site Scripting (XSS)
test('input do usuário não é executado como script', async ({ page }) => {
await page.goto('/login');
// Fazer login primeiro
await page.fill('[data-testid="email"]', 'user@test.com');
await page.fill('[data-testid="password"]', 'SenhaValida1');
await page.click('[data-testid="submit"]');
// Tentar injetar script em um campo controlado pelo usuário
await page.goto('/profile');
const xssPayload = '<script>alert("XSS")</script>';
await page.fill('[data-testid="display-name"]', xssPayload);
await page.click('[data-testid="save"]');
// Navegar para outra página e voltar para ver se o script executa
await page.goto('/dashboard');
await page.goto('/profile');
// Verificar se um dialog aparece (apareceria se o XSS funcionasse)
let dialogAppeared = false;
page.on('dialog', dialog => {
dialogAppeared = true;
dialog.dismiss();
});
await page.waitForTimeout(1000); // Dar tempo para o script executar se estiver presente
expect(dialogAppeared).toBe(false);
// O nome deve aparecer como texto, não ser executado
const nameField = page.getByTestId('display-name');
const value = await nameField.inputValue();
expect(value).toBe(xssPayload); // Armazenado como texto, não executado
});5. Exposição de dados sensíveis
test('senha não é retornada na resposta da API', 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('token de auth não aparece nos headers de resposta', async ({ request }) => {
const response = await request.post('/api/auth/login', {
data: { email: 'user@test.com', password: 'SenhaValida1' },
});
// Token deve estar no body, não exposto em headers para todos
const headers = response.headers();
expect(headers['x-auth-token']).toBeUndefined();
expect(headers['authorization']).toBeUndefined();
// Mas deve estar no body
const body = await response.json();
expect(body.token).toBeDefined();
});
test('API usa HTTPS', async ({ request }) => {
const response = await request.get('/api/users');
// Verificar header HSTS
const headers = response.headers();
expect(headers['strict-transport-security']).toBeDefined();
});OWASP ZAP: scanning de segurança automatizado
O OWASP ZAP é uma ferramenta gratuita de scanning de segurança. Você pode integrá-la com o Playwright:
// Usando proxy do ZAP com Playwright
test.use({ proxy: { server: 'http://localhost:8080' } }); // proxy do ZAP
test('scan de segurança via ZAP', async ({ page }) => {
await page.goto('/');
await page.goto('/login');
await page.goto('/products');
// O ZAP faz scan passivo de todas as requisições feitas pelo browser
});Ou rode o ZAP programaticamente:
# Scan do ZAP via Docker
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://staging.myapp.com \
-r zap-report.htmlVerificação de security headers
test('security headers presentes', async ({ request }) => {
const response = await request.get('/');
const headers = response.headers();
// Content Security Policy
expect(headers['content-security-policy']).toBeDefined();
// Prevenir clickjacking
expect(headers['x-frame-options']).toBeDefined();
// Prevenir sniffing de tipo MIME
expect(headers['x-content-type-options']).toBe('nosniff');
// Proteção XSS (legado, mas recomendado)
expect(headers['x-xss-protection']).toBeDefined();
// Apenas HTTPS
expect(headers['strict-transport-security']).toBeDefined();
// Não enviar informações de referência para sites externos
expect(headers['referrer-policy']).toBeDefined();
});Testando casos extremos de autorização
test.describe('IDOR (Referência Direta Insegura a Objetos)', () => {
test('usuário não consegue deletar post de outro usuário', async ({ request }) => {
// Obter tokens de dois usuários diferentes
const user1Token = await getToken('user1@test.com', 'Pass1');
const user2Token = await getToken('user2@test.com', 'Pass2');
// Usuário 2 cria um post
const createResp = await request.post('/api/posts', {
data: { title: 'Post do Usuário 2', content: 'Conteúdo' },
headers: { Authorization: `Bearer ${user2Token}` },
});
const post = await createResp.json();
// Usuário 1 tenta deletar o post do Usuário 2
const deleteResp = await request.delete(`/api/posts/${post.id}`, {
headers: { Authorization: `Bearer ${user1Token}` },
});
// Deve ser 403 Forbidden
expect(deleteResp.status()).toBe(403);
// Post ainda deve existir
const getResp = await request.get(`/api/posts/${post.id}`);
expect(getResp.status()).toBe(200);
});
});Resumo
Testes de segurança que o QA deve sempre incluir:
| Teste | O que verifica |
|-------|----------------|
| Controle de acesso | Usuários não conseguem acessar recursos acima do seu papel |
| IDOR | Usuários não conseguem acessar recursos de outros usuários pelo ID |
| Bloqueio de conta | Proteção contra brute force |
| SQL injection | Input do usuário não é interpretado como SQL |
| XSS | Input do usuário não é executado como script |
| Exposição de dados | Campos sensíveis não são retornados na API |
| Security headers | Headers de HTTPS, CSP e proteção XSS presentes |
Não é necessário encontrar exploits de dia zero. Detectar e prevenir as vulnerabilidades do OWASP Top 10 torna a aplicação dramaticamente mais segura e impede os ataques mais comuns que realmente acontecem no mundo real.
→ Veja também: Fundamentos de Testes de Segurança que Todo Engenheiro QA Deve Conhecer | Autenticação em Testes de API: Chaves API, Tokens Bearer, OAuth2, JWT | O que é uma API REST? Um Guia Prático para Engenheiros QA