Broken Access Control у Node.js і Next.js – практичний посібник з RBAC і NextAuth

Крок за кроком покажемо, як створити надійну систему контролю доступу в Node.js/Next.js, поєднавши NextAuth з RBAC, щоб усунути ризик Broken Access Control.

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

Сучасні веб‑додатки, створені на Node.js та Next.js, часто обслуговують сотні, а іноді й тисячі користувачів з різними рівнями прав. Неправильний контроль доступу є однією з найчастіших причин порушень безпеки – класифікованою в OWASP Top 10 як Broken Access Control. У цій статті ми представляємо практичний підхід до побудови системи RBAC (Role‑Based Access Control), заснованої на NextAuth, яка відповідає вимогам OWASP, CWE та принципу least‑privilege.

Модель загрози – що може піти не так?

Базовий вектор атаки полягає у отриманні неавторизованого доступу до ресурсів, які мають бути захищені. Типові сценарії:

  • Користувач із нижчим рівнем прав (наприклад, гість) отримує доступ до endpoint‑у, призначеного виключно для адміністраторів.
  • Маніпуляція параметрами в URL або в тілі запиту дозволяє обійти перевірку прав.
  • Сесія перехоплюється і використовується після закінчення терміну дії або після зміни ролі.

Усі ці випадки описані в CWE‑284 (Improper Access Control) та CWE‑285 (Improper Authorization). Ефективний захист вимагає послідовної перевірки прав на кожному рівні застосунку – від шару маршрутизації до шару даних.

Механізм – як працює RBAC у NextAuth?

NextAuth забезпечує готову систему автентифікації (session, JWT) та хуки, які дозволяють впровадити власну логіку авторизації. Ключові елементи:

  • callbacks.jwt – збагачує токен списком ролей, отриманих з бази.
  • callbacks.session – надає ролі в об’єкті session.user на боці клієнта.
  • Middleware (next.config.js або окремий файл middleware.ts) – централізовано перевіряє, чи має поточний користувач необхідні права для даного шляху.

У поєднанні з простою таблицею ролей і прав у базі (наприклад, PostgreSQL) ми отримуємо повну модель RBAC, яку легко розширити додатковими атрибутами (наприклад, діапазоном організації).

Приклад: вразливий код vs виправлена реалізація

Для початку розглянемо фрагмент, у якому відсутня перевірка ролі – типова помилка, що призводить до Broken Access Control.

// vulnerable endpoint API 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 });
  // лише перевіряємо, чи користувач залогінений
  if (!session) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // виконуємо адміністративну операцію
  const users = await db.user.findMany();
  res.status(200).json(users);
}

Будь‑який залогінений користувач, незалежно від ролі, може отримати список усіх користувачів – класичний приклад CWE‑285.

Виправлена версія використовує ролі, збережені в токені, та центральний middleware.

// middleware.ts – глобальний контроль доступу
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;

  // мапування шляхів на необхідні ролі
  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();
}

Тепер доступ до /api/admin мають лише користувачі з роллю admin. Додатково у самому handler‑і можна виконати додаткову контекстну валідацію.

Чек‑лісти – що потрібно перевірити, щоб уникнути Broken Access Control

  • Переконайтеся, що JWT‑токен містить список ролей і підписаний сильним алгоритмом (HS256 або RS256).
  • Усі API‑endpoint‑и та захищені сторінки мають проходити через middleware або спеціалізовані функції авторизації.
  • Застосовуйте принцип least‑privilege – надавайте мінімальний набір прав, необхідний для виконання завдання.
  • Регулярно тестуйте права за допомогою інструментів пенетраційного тестування (наприклад, OWASP ZAP) та автоматичних статичних сканерів.
  • Логуйте відмови в доступі з інформацією про відсутню роль, але не розкривайте деталі ресурсів.
«Найкращий контроль доступу – це той, який неможливо обійти, бо не існує місця, де його можна обійти.»

Типові помилки та компроміси в реалізації RBAC

Even experienced teams popełniają pewne pułapki:

  • Hardcoding ролей у коді – ускладнює подальші розширення та призводить до розбіжностей між кодом і базою даних.
  • Відсутність оновлення токену після зміни ролі – користувач може зберігати старі права доступу до закінчення сесії.
  • Використання лише фронт‑ендової верифікації – не захищає від прямого виклику endpoint‑у.
  • Over‑permissive типові правила – наприклад, надання ролі user всім новим обліковим записам без додаткової верифікації.

Рішення:

  • Зберігайте визначення ролей і прав у центральній таблиці, а не в коді.
  • Реалізуйте механізм revocation list або короткий TTL токенів, щоб примушувати оновлення після зміни ролі.
  • Завжди перевіряйте права на стороні сервера – фронт‑енд може лише відображати UI.
  • Використовуйте шаблон «deny‑by‑default» – відсутність явної згоди = відмова.

Практичний приклад: повний потік від входу до авторизації

Нижче повний міні‑проект на TypeScript, який поєднує NextAuth, PostgreSQL та RBAC.

// lib/prisma.ts – клієнт Prisma (ORM)
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();

// prisma schema (скорочено)
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 – конфігурація 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 && /* тут верифікація пароля */ 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; // запам'ятовуємо ролі в JWT
      return token;
    },
    async session({ session, token }) {
      session.user.roles = token.roles as string[];
      return session;
    },
  },
  secret: process.env.NEXTAUTH_SECRET,
});

// middleware.ts – контроль доступу
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();
}

Цей код відповідає всім вищезазначеним правилам: ролі зберігаються в базі, токен збагачений ролями, middleware забезпечує доступ на основі мапи protectedRoutes, а UI може користуватись session.user.roles для приховування елементів.

Підсумок і подальші кроки

Впровадження надійної системи контролю доступу в Node.js та Next.js не вимагає складних рішень – ключове це послідовне використання middleware, зберігання ролей у безпечному токені та дотримання принципів least‑privilege і deny‑by‑default. Регулярні ревізії коду, тести авторизації та оновлення документації допомагають підтримувати безпеку на рівні, відповідному OWASP Top 10 та CWE‑284/285.

Якщо вам потрібна підтримка при аудиті існуючих застосунків або ви хочете з нуля побудувати безпечну архітектуру з використанням NextAuth та RBAC, зв’яжіться з командою Coderia.it – ми допоможемо перетворити ваші рішення на міцний фундамент для майбутнього розвитку.

Почнімо

Маєте проєкт на думці?

Опишіть його кількома реченнями. Відповім протягом 24 годин із безкоштовною оцінкою та пропозицією стеку.