Безпека даних у Next.js: приховування ключів і проксіювання API

Як захистити застосунки на Next.js за допомогою патерну BFF: валідуємо змінні оточення, приховуємо URL бекенду та запобігаємо критичним вебатакам.

Vladyslav Tsyvinda
Vladyslav Tsyvinda
·Оновлено
Обкладинка: Безпека даних у Next.js: приховування ключів і проксіювання API

Безпека даних у Next.js: приховування ключів, чутливих даних і проксіювання API

Безпека сучасних вебзастосунків — це не лише HTTPS і базові правила CORS. Коли ми будуємо складні системи з розділеною архітектурою, де фронтенд (наприклад, адмінпанель на Next.js) і бекенд-сервіс (API на Go, Node.js чи Python) існують як окремі сутності, питання приховування внутрішньої інфраструктури стає критичним.

Якщо дозволити браузеру користувача звертатися до бекенду напряму, ви фактично викладаєте карти своєї інфраструктури на стіл перед зловмисниками. Next.js завдяки своїй гібридній природі (Server-Side і Client-Side) дає змогу реалізувати архітектурний патерн BFF (Backend-for-Frontend). У цій моделі Next.js виступає проміжним шаром (Middleman) і єдиною точкою входу для клієнта, надійно захищаючи внутрішні сервіси.


1. Вектори атак: від чого захищає приховування інфраструктури?

Коли логіка запитів, секретні ключі та справжні ендпоінти API лишаються на клієнті, застосунок стає вразливим до цілого ряду серйозних загроз. Проксіювання через Next.js допомагає нейтралізувати такі вектори атак:

1.1. Витік токенів і Credential Stuffing

Якщо ви використовуєте сторонні сервіси (платіжні шлюзи, сервіси email-розсилок) або внутрішні мікросервіси, їм потрібні API-ключі. Запит із браузера означає, що ключ неминуче потрапить у клієнтський JavaScript-бандл. Зловмисник легко витягне його через вкладку Network у DevTools або простим пошуком по JS-файлах, щоб потім витрачати ваші платні квоти чи користуватися вашими обліковими даними.

1.2. Пряма DDoS-атака на бекенд (Direct-to-Origin Attacks)

Якщо клієнтський застосунок робить запити безпосередньо на https://api.my-backend.com, адреса вашого сервера стає публічною. Навіть якщо основний сайт захищений Cloudflare, зловмисник може надсилати тисячі важких запитів (наприклад, зі складною фільтрацією чи генерацією звітів) прямо на IP бекенду в обхід захисних фільтрів — і сервер просто впаде.

1.3. Підміна параметрів і скрейпінг даних

Коли браузер спілкується з API напряму, зловмисник бачить структуру відповідей. Він може спробувати змінити параметри в тілі запиту (наприклад, підставити role=admin замість role=user) або масово вивантажити конфіденційні дані (скрейпінг), адже бачить «сирий» JSON зі службовими полями бази даних.

1.4. Обхід і компрометація CORS

Щоб браузер міг робити запити до API на іншому домені чи порту, розробники часто послаблюють конфігурацію CORS (наприклад, дозволяють Access-Control-Allow-Origin: *). Це відчиняє двері для атак Cross-Site Request Forgery (CSRF). Проксіювання розв’язує цю проблему: клієнт звертається до того самого домену, на якому розміщено Next.js, тож проблеми з CORS просто не виникає.


2. Базова гігієна безпеки та валідація змінних оточення

У Next.js є чітка межа між серверним і клієнтським оточенням. Проте помилка в префіксі може поставити під загрозу безпеку всього застосунку.

Серверні vs клієнтські змінні

За замовчуванням усі змінні у файлі .env.local доступні виключно в середовищі Node.js (на сервері).

# Цей ключ НІКОЛИ не потрапить у браузер
DATABASE_URL="postgresql://user:pass@localhost:5432/db"
STRIPE_SECRET_KEY="sk_test_12345"
INTERNAL_BACKEND_URL="http://backend-service:8080"

# Ця змінна ПОТРАПИТЬ у клієнтський JavaScript завдяки префіксу
NEXT_PUBLIC_ANALYTICS_ID="G-12345678"

Золоте правило: ніколи не додавайте префікс NEXT_PUBLIC_ до токенів авторизації, ключів бази даних чи URL внутрішніх мікросервісів.

Сувора валідація за допомогою Zod

Щоб застосунок не падав у рантаймі через забуту змінну в Docker-контейнері чи CI/CD-пайплайні, валідуйте .env ще на етапі збирання проєкту за допомогою бібліотеки zod.

// src/env.ts
import { z } from 'zod';

const serverSchema = z.object({
  NODE_ENV: z.enum(['development', 'test', 'production']),
  INTERNAL_BACKEND_URL: z.string().url(),
  SERVER_TO_SERVER_SECRET: z.string().min(10),
});

const _serverEnv = serverSchema.safeParse(process.env);

if (!_serverEnv.success) {
  console.error('❌ Invalid environment variables:', _serverEnv.error.format());
  throw new Error('Environment variable validation failed');
}

export const env = _serverEnv.data;

Тепер у серверному коді ви імпортуєте env із файлу src/env.ts і отримуєте повну типізацію та впевненість, що всі критичні дані на місці.


3. Безпека на рівні архітектури: Server Components і Server Actions

З появою App Router (app/) компоненти за замовчуванням стали Server Components. Їхній код виконується виключно на сервері, а клієнт отримує лише згенерований HTML.

Якщо робити запит до бази даних чи стороннього API безпосередньо із Server Component, браузер не бачить ані логіки запиту, ані жодних секретних токенів:

// app/dashboard/page.tsx (Server Component)
import { env } from '@/src/env';

export default async function DashboardPage() {
  // Запит виконується на Node.js-сервері під час рендерингу сторінки
  const res = await fetch(`${env.INTERNAL_BACKEND_URL}/v1/metrics`, {
    headers: {
      'Authorization': `Bearer ${process.env.SECRET_API_TOKEN}`
    }
  });

  const data = await res.json();

  return (
    <main>
      <h1>Metrics Dashboard</h1>
      {/* Клієнт отримує чистий HTML, структура API прихована */}
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </main>
  );
}

Той самий підхід працює і для мутацій даних через Server Actions ("use server"). Уся бізнес-логіка, контроль доступу та робота з ключами лишаються за зачиненими дверима сервера.


4. Проксіювання запитів і маскування URL бекенду

Коли клієнтській частині застосунку (наприклад, інтерактивній таблиці з пагінацією чи динамічній формі) потрібно самостійно надсилати запити, ми маскуємо справжній бекенд. Зробити це можна двома способами.

Варіант A: Next.js Rewrites (просте перенаправлення)

Якщо потрібно лише змінити назву ендпоінта, щоб приховати справжній хост бекенду, використовуйте rewrites у конфігурації.

// next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: '/api/proxy/:path*',
        destination: 'https://real-backend-service.com/api/:path*',
      },
    ]
  },
}

Як це працює: браузер робить запит fetch('/api/proxy/users'). Next.js перехоплює його і на рівні сервера проксіює на real-backend-service.com. Клієнт бачить лише URL вашого власного сайту.

Варіант B: Route Handlers / API Routes (максимальна безпека)

Rewrites не підійдуть, якщо перед надсиланням запиту на бекенд потрібно додати серверний токен, перевірити сесію користувача чи відфільтрувати дані. У такому разі використовують Route Handlers.

1. Серверний проксі-шар з інʼєкцією секретів і санітизацією даних:

// app/api/internal/users/route.ts
import { NextResponse } from 'next/server';
import { env } from '@/src/env';

export async function POST(request: Request){
  try {
    // Отримуємо дані від клієнта
    const body = await request.json();

    // Тут можна додати валідацію через Zod перед відправкою на бекенд

    // Формуємо запит до ізольованого бекенду
    const response = await fetch(`${env.INTERNAL_BACKEND_URL}/v1/users`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        // Додаємо секретний ключ, якого немає і не може бути на клієнті
        'X-Internal-Secret': env.SERVER_TO_SERVER_SECRET,
        'X-Forwarded-For': request.headers.get('x-forwarded-for') || 'unknown',
      },
      body: JSON.stringify(body),
    });

    if (!response.ok) {
      console.error(`Backend error: ${response.status}`);
      return NextResponse.json(
        { error: 'Failed to create user. Please contact support.' },
        { status: response.status }
      );
    }

    const data = await response.json();

    // САНІТИЗАЦІЯ ДАНИХ
    // Видаляємо чутливі поля внутрішньої БД перед відправкою в браузер
    delete data.internal_database_id;
    delete data.password_hash_algorithm;
    delete data.internal_flags;

    return NextResponse.json(data);

  } catch (error) {
    console.error('Proxy Error:', error);
    return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
  }
}

2. Безпечний клієнтський компонент:

'use client'

export function CreateUserForm() {
  const handleCreate = async (formData) => {
    // Клієнт знає лише локальний відносний шлях
    const res = await fetch('/api/internal/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(formData),
    });

    const result = await res.json();
    if (res.ok) alert('User created successfully!');
  };

  // ... рендеринг форми
}

5. Синергія інфраструктури: Docker, Nginx і Cloudflare

Архітектурний патерн BFF працює на всі 100% лише тоді, коли його підтримує правильно налаштована серверна інфраструктура для деплою (наприклад, DigitalOcean Droplet або приватна хмара).

Ізоляція на рівні мережі (Docker)

Контейнери Next.js і бекенду мають бути в одній ізольованій віртуальній мережі (Docker bridge network). У файлі docker-compose.yml для контейнера бекенду блок ports не вказується взагалі (жодного мапінгу на кшталт 8080:8080). Порти бекенду не виставлені назовні. Контейнер Next.js спілкується з ним за внутрішнім DNS-імʼям сервісу: http://backend-api:8080. Дістатися до бекенду ззовні фізично неможливо.

Nginx як Reverse Proxy

Перед Next.js-застосунком стоїть Nginx, який відповідає за термінацію SSL-сертифікатів і первинний захист:

  • Обмеження максимального розміру завантажуваних файлів (client_max_body_size 10M).
  • Фільтрація підозрілих або порожніх заголовків User-Agent.
  • Перенаправлення всього HTTP-трафіку на HTTPS.

Налаштування Edge-захисту в Cloudflare

Оскільки всі клієнтські запити (зокрема операції з даними) тепер проходять через єдиний домен Next.js, інструменти Cloudflare WAF можна використовувати на повну:

  • Rate Limiting: налаштуйте правило, яке блокуватиме IP-адресу, якщо з неї надходить понад 10 запитів на секунду до ендпоінтів /api/internal/*. Це захистить ваші проксі-маршрути від брутфорсу та скрейпінгу.
  • Cache Rules: якщо ваші Route Handlers повертають статичну або рідко оновлювану інформацію (наприклад, довідники, каталоги товарів), можна дозволити Cloudflare кешувати відповіді цих API-ендпоінтів. Наступні запити користувачів обслуговуватимуться миттєво з edge-серверів Cloudflare, взагалі не навантажуючи ні Next.js, ні ваш основний бекенд.

Висновок

Впровадження патерну BFF на Next.js створює надійний захисний щит навколо вашої бізнес-логіки та баз даних. Поєднуючи суворий контроль оточення через Zod, ізоляцію серверного коду в Server Components, гнучке проксіювання через Route Handlers і закриту інфраструктуру на рівні Docker та Cloudflare, ви реалізуєте концепцію Defense in Depth (ешелонованого захисту) і робите застосунок стійким до більшості сучасних кіберзагроз.