← 전체 글로 돌아가기

Next.js

Next.js 블로그에 관리자 페이지 만들 때 보안에서 놓치기 쉬운 것들

미들웨어 인증 누락, API 라우트 노출, 빌드 타임 시크릿 유출 등 개인 블로그 관리자 구현에서 직접 겪은 보안 실수를 정리했다.

개인 블로그에 글 작성 관리자 페이지를 Next.js로 만들면서 "어차피 나만 쓰는 거니까"라는 안일한 생각으로 보안을 소홀히 했다가 나중에 손봐야 했던 부분들이 있다.

/admin 경로를 미들웨어 없이 놔두면 안 된다

/admin 페이지 컴포넌트 안에서만 인증을 체크하는 방식은 위험하다. 서버 컴포넌트나 클라이언트 컴포넌트 모두, 렌더링 자체가 요청을 처리하고 나서 일어나기 때문에 페이지가 로드되는 순간 이미 일부 데이터를 불러올 수 있다.

middleware.ts에서 요청 단계에 인증을 걸어야 안전하다.

// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  if (!token && request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

export const config = {
  matcher: ['/admin/:path*'],
}

API 라우트도 반드시 인증을 체크해야 한다

/api/posts 같은 라우트에서 GET은 열어두더라도, POST/PUT/DELETE는 인증된 사용자만 쓸 수 있어야 한다. 프론트에서 관리자 UI를 숨겼다고 API까지 보호된 건 아니다.

// app/api/posts/route.ts
export async function POST(req: Request) {
  const session = await getServerSession()
  if (!session) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }
  // ...
}

NEXT_PUBLIC_ 접두사를 잘못 쓰면 시크릿이 노출된다

NEXT_PUBLIC_으로 시작하는 환경변수는 클라이언트 번들에 그대로 포함된다. DB 연결 문자열이나 API 시크릿에 이 접두사를 붙이면 브라우저 소스에서 누구나 볼 수 있다.

빌드 후 .next/static 디렉토리에서 직접 검색해보면 확인할 수 있다.

grep -r "DATABASE_URL" .next/static/

서버에서만 쓰는 값은 절대 NEXT_PUBLIC_을 붙이지 않는다.

에러 메시지에 내부 정보를 담지 않는다

관리자 API에서 에러가 나면 그 내용을 그대로 응답으로 보내는 경우가 있다. 스택 트레이스나 DB 쿼리 내용이 응답에 들어가면 공격자에게 힌트가 된다.

운영 환경에서는 클라이언트에게는 일반적인 에러 메시지만 보내고, 세부 내용은 서버 로그에만 남긴다.

개인 블로그라도 인터넷에 공개된 이상 보안을 대충 짜두면 언젠가 문제가 생긴다. 특히 미들웨어 인증 하나만 제대로 잡아도 대부분의 경로를 막을 수 있다.