Next.js / Аюулгүй байдлын үндэс

Аюулгүй байдлын үндэс

Аппын аюулгүй байдал бол хожим засдаг зүйл биш — эхнээс нь зөв тохируулах ёстой. Халдагч нар Next.js, Supabase апп руу XSS, хүсэлт дахь нууц мэдээлэл задруулах, нэвтрэлт тэнцэх гэх мэт аргаар довтолдог. Энэ хичээлд хамгийн түгээмэл аюул болон тэдгээрээс хамгаалах Next.js-ийн built-in механизмуудыг сурна.

Environment variable аюулгүй хэрэглэх

Нууц мэдээлэл (API key, service role key) хэзээ ч client-side кодод очих ёсгүй. Next.js NEXT_PUBLIC_ угтвараар тэднийг ялгадаг:

bash
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co      # хөтөч харж болно
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...                   # хөтөч харж болно
SUPABASE_SERVICE_ROLE_KEY=eyJ...                       # зөвхөн сервер!

SUPABASE_SERVICE_ROLE_KEY-г зөвхөн /app/api route handler дотор хэрэглэнэ:

typescript
// app/api/progress/route.ts  ← ЗӨВШӨӨРӨГДСӨН
import { createClient } from "@supabase/supabase-js";

const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!, // зөвхөн server-side
);
tsx
// components/SomeComponent.tsx  ← БУРУУ — хөтөч харна!
const key = process.env.SUPABASE_SERVICE_ROLE_KEY; // undefined болно, гэхдээ бичих хэрэггүй

Authentication шалгалт

API route бүрт session шалгалт хийнэ. Шалгалтгүй route нь халдагчид чөлөөтэй хэрэглэх боломж олгодог:

typescript
// app/api/progress/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function POST(request: NextRequest) {
  const cookieStore = await cookies();

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { cookies: { getAll: () => cookieStore.getAll() } },
  );

  // Session заавал шалгана
  const {
    data: { session },
  } = await supabase.auth.getSession();

  if (!session) {
    return NextResponse.json({ error: "Нэвтрээгүй байна" }, { status: 401 });
  }

  // session.user.id ашиглан ажилна — итгэж болно
  const userId = session.user.id;

  // ...
  return NextResponse.json({ success: true });
}

Middleware ашиглан хамгаалах

Protected route-г нэг middleware-д бүгдийг шалгавал давтамж арилна:

typescript
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";

const protectedRoutes = ["/profile", "/courses"];

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  const isProtected = protectedRoutes.some((route) =>
    pathname.startsWith(route),
  );

  if (!isProtected) return NextResponse.next();

  const response = NextResponse.next();

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll: () => request.cookies.getAll(),
        setAll: (cookiesToSet) => {
          cookiesToSet.forEach(({ name, value, options }) => {
            response.cookies.set(name, value, options);
          });
        },
      },
    },
  );

  const {
    data: { session },
  } = await supabase.auth.getSession();

  if (!session) {
    const loginUrl = new URL("/login", request.url);
    loginUrl.searchParams.set("redirectTo", pathname);
    return NextResponse.redirect(loginUrl);
  }

  return response;
}

export const config = {
  matcher: ["/profile/:path*", "/courses/:path*"],
};

Input validation хийх

Хэрэглэгчийн оруулсан өгөгдлийг хэзээ ч итгэж болохгүй — заавал шалгана:

typescript
// app/api/progress/route.ts
interface ProgressBody {
  lessonSlug: string;
  courseSlug: string;
  passed: boolean;
}

function isValidProgressBody(body: unknown): body is ProgressBody {
  if (typeof body !== "object" || body === null) return false;
  const b = body as Record<string, unknown>;
  return (
    typeof b.lessonSlug === "string" &&
    b.lessonSlug.length > 0 &&
    b.lessonSlug.length < 100 &&
    typeof b.courseSlug === "string" &&
    b.courseSlug.length > 0 &&
    b.courseSlug.length < 100 &&
    b.passed === true
  );
}

export async function POST(request: NextRequest) {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return NextResponse.json(
      { error: "JSON буруу форматтай байна" },
      { status: 400 },
    );
  }

  if (!isValidProgressBody(body)) {
    return NextResponse.json({ error: "Өгөгдөл буруу байна" }, { status: 400 });
  }

  // body.lessonSlug, body.courseSlug найдвартай болсон
}

Security header тохируулах

next.config.ts-д HTTP security header нэмнэ:

typescript
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  async headers() {
    return [
      {
        source: "/(.*)",
        headers: [
          { key: "X-Frame-Options", value: "DENY" },
          { key: "X-Content-Type-Options", value: "nosniff" },
          { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
          {
            key: "Permissions-Policy",
            value: "camera=(), microphone=(), geolocation=()",
          },
        ],
      },
    ];
  },
};

export default nextConfig;

| Аюул | Хамгаалалт | | ------------------ | ---------------------------- | | Нууц key задрах | NEXT_PUBLIC_ зөв хэрэглэх | | Нэвтрэлгүй хандалт | Middleware + session шалгалт | | Буруу оролт | Input validation | | Clickjacking | X-Frame-Options header | | SQL injection | Supabase parameterized query |

Supabase Row Level Security (RLS) нь database түвшинд хамгаалалт нэмдэг — API route-н шалгалттай хослуулахад давхар хамгаалалт бүрддэг. RLS-г заавал идэвхжүүлж, policy-г зөв тохируулна.

Дараагийн хичээлд:

Vercel дээр Next.js апп deploy хийх дэлгэрэнгүй алхмуудыг сурна. Production орчинд environment variable тохируулж, домэйнтэй апп нийтлэх аргыг дадлагажина.