Les ingénieurs QA peuvent tester les vulnérabilités de sécurité web les plus courantes sans expertise en test de pénétration. L'injection SQL, le XSS, les problèmes d'authentification, les références directes à des objets non sécurisées et les vérifications d'autorisation manquantes sont tous testables pendant les tests fonctionnels habituels.

Pourquoi le QA doit faire des tests de sécurité

La sécurité est la responsabilité de tous. Les ingénieurs QA ont des avantages :

  • Accès au code source et aux environnements de test, que les pentesters n'ont souvent pas
  • Compréhension de la logique applicative : vous savez ce qui devrait et ne devrait pas être possible
  • Infrastructure de tests automatisés : les vérifications de sécurité peuvent faire partie de votre pipeline CI
  • Cadence de test fréquente : les régressions de sécurité sont détectées tôt

Le Top 10 OWASP (le plus pertinent pour le QA)

L'Open Web Application Security Project publie les vulnérabilités web les plus courantes. Voici celles que vous rencontrerez le plus souvent.

1. Contrôle d'accès défaillant

Des utilisateurs accèdent à des ressources auxquelles ils ne devraient pas avoir accès.

Test :

test('regular user cannot access admin API', async ({ request }) => {
  // Connexion en tant que membre standard
  const loginResp = await request.post('/api/auth/login', {
    data: { email: 'member@test.com', password: 'MemberPass1' },
  });
  const { token } = await loginResp.json();

  // Tentative d'accès à l'endpoint admin
  const response = await request.get('/api/admin/users', {
    headers: { Authorization: `Bearer ${token}` },
  });

  // Doit être 403 Forbidden, pas 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');

  // L'utilisateur 1 tente d'accéder au profil de l'utilisateur 2 en devinant l'ID
  const response = await request.get('/api/users/999', {
    headers: { Authorization: `Bearer ${user1Token}` },
  });

  // Doit être 403 ou 404, pas 200 avec les données de l'utilisateur 2
  expect([403, 404]).toContain(response.status());
});

2. Problèmes d'authentification

test('account lockout after failed attempts', async ({ request }) => {
  // Essayer le mauvais mot de passe 5 fois
  for (let i = 0; i < 5; i++) {
    await request.post('/api/auth/login', {
      data: { email: 'user@test.com', password: 'WrongPassword' },
    });
  }

  // La 6e tentative doit être bloquée
  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 }) => {
  // Demander une réinitialisation
  await request.post('/api/auth/forgot-password', {
    data: { email: 'user@test.com' },
  });

  // En test, utiliser un token expiré depuis la base de données ou un mock
  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. Injection SQL

test('login form is not SQL injectable', async ({ page }) => {
  await page.goto('/login');

  // Payload d'injection SQL classique
  await page.fill('[data-testid="email"]', "' OR '1'='1");
  await page.fill('[data-testid="password"]', "' OR '1'='1");
  await page.click('[data-testid="submit"]');

  // Ne doit PAS être connecté
  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}` },
    });

    // Doit retourner 200 avec des résultats vides/sûrs, pas une erreur ou un dump de données
    expect(response.status()).toBe(200);
    const body = await response.json();

    // Ne doit pas retourner tous les utilisateurs (ce qui indiquerait une injection réussie)
    const count = Array.isArray(body.data) ? body.data.length : body.length;
    expect(count).toBeLessThan(100);
  }
});

4. Cross-Site Scripting (XSS)

test('user input is not executed as script', async ({ page }) => {
  await page.goto('/login');

  // Connexion d'abord
  await page.fill('[data-testid="email"]', 'user@test.com');
  await page.fill('[data-testid="password"]', 'ValidPass1');
  await page.click('[data-testid="submit"]');

  // Tentative d'injection de script dans un champ contrôlé par l'utilisateur
  await page.goto('/profile');

  const xssPayload = '<script>alert("XSS")</script>';
  await page.fill('[data-testid="display-name"]', xssPayload);
  await page.click('[data-testid="save"]');

  // Naviguer ailleurs puis revenir pour voir si le script s'exécute
  await page.goto('/dashboard');
  await page.goto('/profile');

  // Vérifier si une boîte de dialogue apparaît (elle apparaîtrait si le XSS fonctionnait)
  let dialogAppeared = false;
  page.on('dialog', dialog => {
    dialogAppeared = true;
    dialog.dismiss();
  });

  await page.waitForTimeout(1000);

  expect(dialogAppeared).toBe(false);

  // Le nom doit apparaître comme texte, pas être exécuté
  const nameField = page.getByTestId('display-name');
  const value = await nameField.inputValue();
  expect(value).toBe(xssPayload);
});

5. Exposition de données sensibles

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' },
  });

  // Le token doit être dans le body, pas exposé dans les en-têtes
  const headers = response.headers();
  expect(headers['x-auth-token']).toBeUndefined();
  expect(headers['authorization']).toBeUndefined();

  // Mais doit être dans le body
  const body = await response.json();
  expect(body.token).toBeDefined();
});

test('API uses HTTPS', async ({ request }) => {
  const response = await request.get('/api/users');

  // Vérifier la présence de l'en-tête HSTS
  const headers = response.headers();
  expect(headers['strict-transport-security']).toBeDefined();
});

OWASP ZAP : scan de sécurité automatisé

OWASP ZAP est un outil de scan de sécurité gratuit. Il peut s'intégrer avec Playwright :

// Utiliser le proxy ZAP avec Playwright
test.use({ proxy: { server: 'http://localhost:8080' } });  // proxy ZAP

test('security scan through ZAP', async ({ page }) => {
  await page.goto('/');
  await page.goto('/login');
  await page.goto('/products');
  // ZAP scanne passivement toutes les requêtes émises par le navigateur
});

Ou exécutez ZAP par programme :

# Scan ZAP via Docker
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://staging.myapp.com \
  -r zap-report.html

Vérification des en-têtes de sécurité

test('security headers present', async ({ request }) => {
  const response = await request.get('/');
  const headers = response.headers();

  // Politique de sécurité du contenu
  expect(headers['content-security-policy']).toBeDefined();

  // Protection contre le clickjacking
  expect(headers['x-frame-options']).toBeDefined();

  // Empêcher le MIME type sniffing
  expect(headers['x-content-type-options']).toBe('nosniff');

  // Protection XSS (legacy, mais utile)
  expect(headers['x-xss-protection']).toBeDefined();

  // HTTPS uniquement
  expect(headers['strict-transport-security']).toBeDefined();

  // Ne pas envoyer d'informations referrer aux sites externes
  expect(headers['referrer-policy']).toBeDefined();
});

Tester les cas limites d'autorisation

test.describe('IDOR (Insecure Direct Object Reference)', () => {
  test('user cannot delete another user\'s post', async ({ request }) => {
    // Obtenir les tokens de deux utilisateurs différents
    const user1Token = await getToken('user1@test.com', 'Pass1');
    const user2Token = await getToken('user2@test.com', 'Pass2');

    // L'utilisateur 2 crée un post
    const createResp = await request.post('/api/posts', {
      data: { title: 'Post utilisateur 2', content: 'Contenu' },
      headers: { Authorization: `Bearer ${user2Token}` },
    });
    const post = await createResp.json();

    // L'utilisateur 1 tente de supprimer le post de l'utilisateur 2
    const deleteResp = await request.delete(`/api/posts/${post.id}`, {
      headers: { Authorization: `Bearer ${user1Token}` },
    });

    // Doit être 403 Forbidden
    expect(deleteResp.status()).toBe(403);

    // Le post doit toujours exister
    const getResp = await request.get(`/api/posts/${post.id}`);
    expect(getResp.status()).toBe(200);
  });
});

Récapitulatif

Tests de sécurité que le QA doit toujours inclure :

| Test | Ce qu'il vérifie |

|---|---|

| Contrôle d'accès | Les utilisateurs n'accèdent pas aux ressources au-delà de leur rôle |

| IDOR | Les utilisateurs n'accèdent pas aux ressources d'autres utilisateurs par ID |

| Blocage de compte | Protection contre le brute force |

| Injection SQL | Les entrées utilisateur ne sont pas interprétées comme du SQL |

| XSS | Les entrées utilisateur ne sont pas exécutées comme des scripts |

| Exposition de données | Les champs sensibles ne sont pas retournés dans l'API |

| En-têtes de sécurité | HTTPS, CSP, en-têtes de protection XSS présents |

Vous n'avez pas besoin de trouver des failles zero-day. Détecter et prévenir les dix vulnérabilités OWASP les plus courantes rend votre application considérablement plus sûre et protège contre les attaques réelles les plus fréquentes.

→ See also: Bases des Tests de Sécurité que Tout Ingénieur QA Devrait Connaître | Authentification dans les Tests d'API: Clés API, Tokens Bearer, OAuth2, JWT | Qu'est-ce qu'une API REST? Un Guide Pratique pour les Ingénieurs QA