Controllo di Accesso Rotto in Node.js e Next.js – guida pratica a RBAC e NextAuth

Passo dopo passo mostreremo come costruire un solido sistema di controllo degli accessi in Node.js/Next.js, integrando NextAuth con RBAC, per eliminare il rischio di Broken Access Control.

Broken Access Control w Node.js i Next.js – praktyczny przewodnik po RBAC i NextAuth

Le moderne applicazioni web costruite con Node.js e Next.js gestiscono spesso centinaia, se non migliaia, di utenti con diversi livelli di autorizzazione. Un controllo degli accessi inadeguato è una delle cause più comuni di violazioni della sicurezza – classificata nell'OWASP Top 10 come Broken Access Control. In questo articolo presentiamo un approccio pratico alla costruzione di un sistema RBAC (Role‑Based Access Control) basato su NextAuth, che soddisfa i requisiti di OWASP, CWE e il principio del least‑privilege.

Modello di minaccia – cosa può andare storto?

Il vettore di attacco di base consiste nell'ottenere accesso non autorizzato a risorse che dovrebbero essere protette. Gli scenari tipici includono:

  • Un utente con un livello di autorizzazione inferiore (ad esempio un ospite) ottiene l'accesso a un endpoint riservato esclusivamente agli amministratori.
  • La manipolazione dei parametri nell'URL o nel corpo della richiesta consente di aggirare la verifica dei permessi.
  • La sessione viene intercettata e utilizzata dopo la scadenza o dopo un cambiamento di ruolo.

Tutti questi casi sono descritti in CWE‑284 (Improper Access Control) e CWE‑285 (Improper Authorization). Una difesa efficace richiede il controllo coerente dei permessi a ogni livello dell'applicazione – dal layer di routing fino al layer dei dati.

Meccanismo – come funziona RBAC in NextAuth?

NextAuth fornisce un sistema di autenticazione pronto all'uso (session, JWT) e hook che consentono di iniettare la propria logica di autorizzazione. Elementi chiave:

  • callbacks.jwt – arricchisce il token con l'elenco dei ruoli prelevato dal database.
  • callbacks.session – espone i ruoli nell'oggetto session.user sul lato client.
  • Middleware (next.config.js o file dedicato middleware.ts) – verifica centralmente se l'utente corrente possiede i permessi richiesti per il percorso specifico.

In combinazione con una semplice tabella di ruoli e permessi nel database (ad esempio PostgreSQL) otteniamo un modello RBAC completo, facilmente estendibile con attributi aggiuntivi (ad esempio l'ambito organizzativo).

Esempio: codice vulnerabile vs implementazione corretta

Per cominciare, diamo un'occhiata a un frammento in cui manca la verifica del ruolo – un errore tipico che porta a Broken Access Control.

// endpoint API vulnerabile in Next.js (pages/api/admin/users.ts)
import { getSession } from 'next-auth/react';
export default async function handler(req, res) {
  const session = await getSession({ req });
  // controlliamo solo se l'utente è autenticato
  if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // eseguiamo l'operazione amministrativa
  const users = await db.user.findMany();
  res.status(200).json(users);
}

Qualsiasi utente autenticato, indipendentemente dal ruolo, può recuperare l'elenco di tutti gli utenti – classico esempio di CWE‑285.

La versione corretta utilizza i ruoli memorizzati nel token e il middleware centrale.

// middleware.ts – controllo di accesso globale
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const { pathname } = req.nextUrl;

  // mappatura dei percorsi sui ruoli richiesti
  const roleMap = {
    '/api/admin': ['admin'],
    '/api/manager': ['admin', 'manager'],
  };

  for (const [prefix, roles] of Object.entries(roleMap)) {
    if (pathname.startsWith(prefix)) {
      if (!token || !roles.some(r => token.roles?.includes(r))) {
        return NextResponse.redirect(new URL('/unauthorized', req.url));
      }
    }
  }
  return NextResponse.next();
}

Ora l'accesso a /api/admin è consentito solo agli utenti con il ruolo admin. Inoltre, all'interno dell'handler è possibile eseguire ulteriori validazioni contestuali.

Checklist – cosa verificare per evitare Broken Access Control

  • Assicurati che il token JWT contenga l'elenco dei ruoli e sia firmato con un algoritmo robusto (HS256 o RS256).
  • Tutti gli endpoint API e le pagine protette devono passare attraverso il middleware o funzioni di autorizzazione dedicate.
  • Applica il principio del least‑privilege – assegna il minimo set di permessi necessario per completare il compito.
  • Esegui test regolari dei permessi usando strumenti di penetration testing (ad esempio OWASP ZAP) e scanner statici automatici.
  • Registra i rifiuti di accesso con l'informazione sul ruolo mancante, ma non rivelare dettagli delle risorse.
«Il miglior controllo degli accessi è quello che non può essere aggirato, perché non esiste alcun punto in cui possa essere bypassato.»

Errori comuni e compromessi nell'implementazione di RBAC

Even experienced teams commettono alcune trappole:

  • Hardcoding dei ruoli nel codice – rende difficili le future estensioni e porta a discrepanze tra codice e database.
  • Mancanza di aggiornamento del token al cambio di ruolo – l'utente può mantenere i vecchi permessi fino alla scadenza della sessione.
  • Utilizzo esclusivo della verifica front‑end – non protegge da chiamate dirette all'endpoint.
  • Regole predefinite troppo permissive – ad esempio assegnare il ruolo user a tutti i nuovi account senza verifica aggiuntiva.

Soluzioni:

  • Conserva le definizioni di ruoli e permessi in una tabella centrale, non nel codice.
  • Implementa un meccanismo di revocation list o token con TTL breve, per forzare l'aggiornamento al cambio di ruolo.
  • Verifica sempre i permessi sul server – il front‑end può solo presentare l'interfaccia UI.
  • Usa il modello “deny‑by‑default” – assenza di consenso esplicito = rifiuto.

Esempio pratico: flusso completo dal login all'autorizzazione

Di seguito un mini‑progetto completo in TypeScript, che combina NextAuth, PostgreSQL e RBAC.

// lib/prisma.ts – client Prisma (ORM)
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

// prisma schema (riassunto)
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  roles     Role[]   @relation("UserRoles")
}
model Role {
  id    Int    @id @default(autoincrement())
  name  String @unique
  users User[] @relation("UserRoles")
}

// pages/api/auth/[...nextauth].ts – configurazione NextAuth
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { prisma } from '../../../lib/prisma';

export default NextAuth({
  providers: [
    CredentialsProvider({
      async authorize(credentials) {
        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
          include: { roles: true },
        });
        if (user && /* qui verifica password */ true) {
          return { id: user.id, email: user.email, roles: user.roles.map(r => r.name) };
        }
        return null;
      },
    }),
  ],
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.roles = user.roles; // memorizziamo i ruoli nel JWT
      return token;
    },
    async session({ session, token }) {
      session.user.roles = token.roles as string[];
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
});

// middleware.ts – controllo accessi
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const url = req.nextUrl.clone();

  const protectedRoutes = [
    { prefix: '/admin', roles: ['admin'] },
    { prefix: '/manager', roles: ['admin', 'manager'] },
  ];

  for (const route of protectedRoutes) {
    if (url.pathname.startsWith(route.prefix)) {
      if (!token?.roles?.some(r => route.roles.includes(r))) {
        url.pathname = '/unauthorized';
        return NextResponse.rewrite(url);
      }
    }
  }
  return NextResponse.next();
}

Questo codice soddisfa tutti i principi sopra elencati: i ruoli sono memorizzati nel database, il token è arricchito con i ruoli, il middleware impone l'accesso basandosi sulla mappa protectedRoutes, e l'UI può utilizzare session.user.roles per nascondere gli elementi.

Riepilogo e prossimi passi

L'implementazione di un solido sistema di controllo degli accessi in Node.js e Next.js non richiede soluzioni complesse – la chiave è l'uso coerente del middleware, la memorizzazione dei ruoli in un token sicuro e il rispetto dei principi di least‑privilege e deny‑by‑default. Revisioni regolari del codice, test di autorizzazione e aggiornamento della documentazione aiutano a mantenere la sicurezza al livello richiesto da OWASP Top 10 e CWE‑284/285.

Se hai bisogno di supporto per audit di applicazioni esistenti o desideri costruire da zero un'architettura sicura con NextAuth e RBAC, contatta il team di Coderia.it – ti aiuteremo a trasformare le tue soluzioni in una base solida per lo sviluppo futuro.

Cominciamo

Hai un progetto in mente?

Descrivilo in poche righe: rispondo entro 24 ore con un preventivo gratuito e una proposta di stack.