Protection des API avec CORS et CSRF

Guide d'implémentation des protections CORS et CSRF pour sécuriser les API web.

Configuration CORS

// Express.js avec cors
const express = require('express');
const cors = require('cors');

const app = express();

// Configuration simple
app.use(cors());

// Configuration détaillée
const corsOptions = {
  origin: ['https://example.com', 'https://admin.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['Content-Range', 'X-Content-Range'],
  credentials: true,
  maxAge: 3600,
  optionsSuccessStatus: 204
};

app.use(cors(corsOptions));

// CORS dynamique
app.use(cors((req, callback) => {
  const origin = req.header('Origin');
  const allowedOrigins = [
    'https://example.com',
    'https://admin.example.com'
  ];

  callback(null, {
    origin: allowedOrigins.includes(origin),
    methods: 'GET,POST,PUT,DELETE',
    credentials: true
  });
}));

// CORS par route
app.get('/api/public', cors(), (req, res) => {
  res.json({ message: 'Public API' });
});

app.get('/api/private', cors(corsOptions), (req, res) => {
  res.json({ message: 'Private API' });
});

Protection CSRF

// Express.js avec csurf
const csrf = require('csurf');
const cookieParser = require('cookie-parser');

// Middleware CSRF
app.use(cookieParser());
app.use(csrf({ cookie: true }));

// Middleware pour envoyer le token
app.use((req, res, next) => {
  res.locals.csrfToken = req.csrfToken();
  next();
});

// Route pour le formulaire
app.get('/form', (req, res) => {
  res.render('form', {
    csrfToken: req.csrfToken()
  });
});

// Protection des routes POST
app.post('/submit', (req, res) => {
  // CSRF vérifié automatiquement
  res.json({ success: true });
});

// Gestion des erreurs CSRF
app.use((err, req, res, next) => {
  if (err.code === 'EBADCSRFTOKEN') {
    res.status(403).json({
      error: 'Session invalide'
    });
  } else {
    next(err);
  }
});

// Template form.ejs
<form action="/submit" method="POST">
  <input type="hidden"
         name="_csrf"
         value="<%= csrfToken %>">
  <!-- autres champs -->
</form>

// React avec Axios
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
axios.defaults.withCredentials = true;

Double protection avec token

// Backend (Express)
const crypto = require('crypto');

// Générer un token
function generateToken() {
  return crypto.randomBytes(32).toString('hex');
}

// Stockage des tokens (en mémoire pour l'exemple)
const tokenStore = new Map();

// Middleware de vérification
const verifyToken = (req, res, next) => {
  const csrfToken = req.header('X-CSRF-Token');
  const sessionToken = tokenStore.get(req.sessionID);

  if (!csrfToken || !sessionToken || csrfToken !== sessionToken) {
    return res.status(403).json({
      error: 'Invalid CSRF token'
    });
  }

  next();
};

// Route pour obtenir un token
app.get('/csrf-token', (req, res) => {
  const token = generateToken();
  tokenStore.set(req.sessionID, token);
  res.json({ token });
});

// Frontend (React)
class Api {
  constructor() {
    this.csrfToken = null;
  }

  async getCsrfToken() {
    if (!this.csrfToken) {
      const response = await fetch('/csrf-token', {
        credentials: 'include'
      });
      const { token } = await response.json();
      this.csrfToken = token;
    }
    return this.csrfToken;
  }

  async post(url, data) {
    const token = await this.getCsrfToken();
    return fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': token
      },
      credentials: 'include',
      body: JSON.stringify(data)
    });
  }
}

Recommandations de sécurité :

  • Toujours utiliser HTTPS
  • Valider l'origine des requêtes
  • Implémenter une double protection (CSRF + token personnalisé)
  • Utiliser SameSite=Strict pour les cookies
  • Nettoyer régulièrement les tokens expirés

Protection WebSocket

// Backend (Express + ws)
const WebSocket = require('ws');
const url = require('url');

const wss = new WebSocket.Server({
  server,
  verifyClient: (info, cb) => {
    const token = url.parse(info.req.url, true).query.token;

    // Vérifier le token
    if (!isValidToken(token)) {
      cb(false, 403, 'Unauthorized');
      return;
    }

    cb(true);
  }
});

wss.on('connection', (ws, req) => {
  const origin = req.headers.origin;
  if (!isAllowedOrigin(origin)) {
    ws.close();
    return;
  }

  ws.on('message', (data) => {
    // Traiter les messages
  });
});

// Frontend
class SecureWebSocket {
  constructor(url) {
    this.url = url;
    this.token = null;
  }

  async connect() {
    if (!this.token) {
      this.token = await this.getCsrfToken();
    }

    this.ws = new WebSocket(
      `${this.url}?token=${this.token}`
    );

    this.ws.onmessage = this.onMessage.bind(this);
  }

  async getCsrfToken() {
    const response = await fetch('/csrf-token', {
      credentials: 'include'
    });
    const { token } = await response.json();
    return token;
  }

  onMessage(event) {
    // Traiter les messages
  }
}

Tests de sécurité

const request = require('supertest');
const app = require('../app');

describe('CORS', () => {
  test('should allow requests from allowed origin', async () => {
    const response = await request(app)
      .get('/api/data')
      .set('Origin', 'https://example.com');

    expect(response.headers['access-control-allow-origin'])
      .toBe('https://example.com');
  });

  test('should reject requests from invalid origin', async () => {
    const response = await request(app)
      .get('/api/data')
      .set('Origin', 'https://evil.com');

    expect(response.headers['access-control-allow-origin'])
      .toBeUndefined();
  });
});

describe('CSRF Protection', () => {
  let csrfToken;

  beforeEach(async () => {
    const response = await request(app)
      .get('/csrf-token')
      .set('Cookie', ['connect.sid=test']);

    csrfToken = response.body.token;
  });

  test('should accept valid CSRF token', async () => {
    const response = await request(app)
      .post('/api/data')
      .set('Cookie', ['connect.sid=test'])
      .set('X-CSRF-Token', csrfToken)
      .send({ data: 'test' });

    expect(response.status).toBe(200);
  });

  test('should reject invalid CSRF token', async () => {
    const response = await request(app)
      .post('/api/data')
      .set('Cookie', ['connect.sid=test'])
      .set('X-CSRF-Token', 'invalid')
      .send({ data: 'test' });

    expect(response.status).toBe(403);
  });
});