Les Server Actions de Next.js exposent automatiquement chaque fonction exportée dans un fichier use-server en tant que point de terminaison (endpoint) HTTP public, en lui attribuant un identifiant unique que le client utilise pour invoquer la fonction. Le point de terminaison existe, qu'un bouton, un lien ou tout autre élément d'interface utilisateur soit rendu ou non, ce qui signifie que toute personne découvrant l'identifiant peut appeler la fonction directement.

La conception du framework traite les Server Actions comme des fonctions utilitaires ordinaires, mais lors de l'exécution, elles deviennent des URL accessibles. Pour les développeurs qui pensaient que l'interface utilisateur était le seul garde-fou, cela crée une surface d'attaque invisible qui peut être exploitée avec une seule requête forgée.

Pourquoi les Server Actions semblaient sûres

Les Server Actions ont été introduites pour permettre aux développeurs d'écrire du code côté serveur juste à côté de leurs composants, évitant ainsi la lourdeur (boilerplate) des routes API séparées. Un usage typique ressemble à ceci :

<form action={deleteInvoice}>
  <button type="submit">Delete</button>
</form>

Comme le formulaire est le seul moyen visible de déclencher deleteInvoice, de nombreux développeurs cachent le bouton pour les utilisateurs non autorisés, pensent que les signatures TypeScript empêcheront les données malformées et comptent sur le fait que la fonction réside dans un module exclusivement serveur. Aucune de ces hypothèses ne fournit de réelle protection.

L'exposition cachée

Lorsqu'un fichier contient “use server”, Next.js compile chaque fonction exportée en un point de terminaison tel que :

POST /_next/data/<build-id>/<page>.json?__rsc=<action-id>

Le <action-id> est un hash stable que le bundle client intègre. Un attaquant peut l'obtenir en :

  • Inspectant le trafic réseau de la page.
  • Lisant le JavaScript compilé (l'ID est une simple chaîne de caractères).
  • Devinant sur la base de conventions de nommage si le projet suit des modèles prévisibles.

Une fois l'ID connu, une requête peut être envoyée depuis n'importe quel outil — cURL, Postman ou un script malveillant — contournant ainsi tous les contrôles au niveau de l'interface utilisateur.

Trois risques concrets

Risque Pourquoi c'est important
Les contrôles UI sont inefficaces Cacher un bouton ou un lien ne supprime pas le point de terminaison sous-jacent. Le point de terminaison reste accessible, tout comme une page d'administration cachée qui existe toujours sur le serveur.
TypeScript n'offre aucune sécurité à l'exécution Les types sont supprimés lors de l'exécution du code. Une fonction déclarée comme deleteInvoice(id: number) peut recevoir une chaîne de caractères massive, un tableau ou même un JSON malveillant, entraînant des erreurs de logique ou des attaques par injection.
Pas d'authentification ou d'autorisation par défaut Les Server Actions ressemblent à des fonctions utilitaires locales, les développeurs oublient donc souvent d'ajouter des vérifications de session, une protection CSRF ou des contrôles de permissions au niveau des lignes (row-level), qui sont pourtant la norme dans les routes API traditionnelles.

Comment sécuriser une Server Action

  1. Authentifier l'appelant – Vérifiez qu'une session ou un jeton valide existe avant l'exécution de toute logique métier.
  2. Valider la charge utile (payload) – Utilisez une bibliothèque de schéma (par exemple, Zod, Yup) pour imposer les types de données et les contraintes de valeur à l'exécution.
  3. Autoriser l'opération – Au-delà de la question « l'utilisateur est-il connecté ? », confirmez que l'utilisateur est bien le propriétaire de l'enregistrement spécifique qu'il tente de modifier ou de supprimer.

Un exemple minimal :

'use server';
import { getSession } from '@/auth';
import { z } from 'zod';
import { db } from '@/db';

const DeleteInvoiceSchema = z.object({
  id: z.number().int().positive(),
});

export async function deleteInvoice(formData: FormData) {
  const session = await getSession();
  if (!session) throw new Error('Unauthenticated');

  const parsed = DeleteInvoiceSchema.safeParse({
    id: Number(formData.get('id')),
  });
  if (!parsed.success) throw new Error('Invalid input');

  const invoice = await db.invoice.findUnique({ where: { id: parsed.data.id } });
  if (!invoice || invoice.ownerId !== session.userId) {
    throw new Error('Unauthorized');
  }

  await db.invoice.delete({ where: { id: invoice.id } });
}

Le code vérifie explicitement l'authentification, valide l' id entrant et s'assure que l'utilisateur connecté est bien le propriétaire de la facture avant de procéder à la suppression.

Autres pièges subtils de Next.js

Problème Symptôme Correction
Variables d'env NEXT_PUBLIC_ Tout ce qui est préfixé par NEXT_PUBLIC_ est inclus dans le bundle client, exposant ainsi des secrets. Gardez les secrets dans des variables d'environnement classiques, ne les préfixez jamais avec NEXT_PUBLIC_.
Redirections ouvertes Accepter un paramètre de requête redirect et le concaténer naïvement peut envoyer les utilisateurs vers //evil.com. Validez la cible par rapport à une liste blanche ou imposez des contrôles de même origine (same-origin).
Server-Side Request Forgery (SSRF) Récupérer une URL fournie par un utilisateur peut permettre à des attaquants d'atteindre des services internes ou des points de terminaison de métadonnées cloud. Utilisez une liste blanche d'hôtes, bloquez les plages d'adresses IP privées et définissez des délais d'attente (timeouts).
dangerouslySetInnerHTML Le rendu de HTML fourni par l'utilisateur sans assainissement (sanitization) ouvre des failles XSS. Utilisez une bibliothèque comme DOMPurify ou évitez complètement le HTML brut.

Rechercher ces modèles automatiquement

Les outils d'analyse statique peuvent signaler les constructions risquées listées ci-dessus. Une option légère est Semgrep, qui s'exécute rapidement et peut être intégré dans les pipelines CI :

npx --yes semgrep --config https://raw.githubusercontent.com/catidegla/stacksec/main/rules .

L'ensemble de règles inclut des vérifications pour les fonctions serveur exportées, l'utilisation abusive de NEXT_PUBLIC_, les redirections ouvertes, les modèles SSRF et l'insertion de HTML non sécurisée.

À surveiller ensuite

  • Mises à jour du framework – Gardez un œil sur les versions de Next.js ; l'équipe pourrait introduire des hooks d'authentification intégrés ou isoler les endpoints générés.
  • Outils communautaires – De nouveaux plugins ESLint et des règles Semgrep spécifiques à Next.js émergent pour automatiser les mesures de protection décrites ici.
  • Incidents en conditions réelles – À mesure que davantage de projets adoptent les Server Actions, surveillez les exploits divulgués qui illustrent le risque en pratique. Une détection précoce peut éclairer les revues de sécurité internes avant qu'une faille ne survienne.

À retenir : Une Server Action Next.js n'est pas une fonction utilitaire privée ; c'est un endpoint HTTP public dès l'instant où vous l'exportez. Traitez-la comme n'importe quelle autre route d'API — authentifiez, validez et autorisez — sinon la commodité d'écrire du code serveur à côté de l'interface utilisateur peut rapidement devenir un risque pour la sécurité.