본문으로 건너뛰기
개발 머꼬
개발 노트Next.js
hohyeon.dev18

Server Action에서 클라이언트 유효성 검사만 믿었다가 생긴 문제

  • #Engineering Note
  • #Forms
  • #Next.js
  • #Security

문제 발생

회원가입 폼에 requiredtype="email" 속성만 걸어뒀는데, 실제로는 형식에 맞지 않는 값이 DB에 저장되는 사례가 발견됐습니다.

<form action={signUpAction}>
  <input name="email" type="email" required />
  <button type="submit">가입</button>
</form>

원인 분석

HTML의 required/type="email"/pattern 같은 속성은 브라우저 UI 차원의 힌트일 뿐입니다. 이 검증은 실제 폼을 렌더링하고 제출하는 브라우저 안에서만 작동합니다. Server Action은 결국 하나의 HTTP 엔드포인트이므로, 브라우저를 거치지 않고 fetchcurl로 직접 그 엔드포인트를 호출하면 HTML 검증은 전혀 적용되지 않고 요청이 그대로 서버 함수까지 도달합니다.

즉 클라이언트 측 검증은 정상적인 사용자에게 빠른 피드백을 주는 UX 장치이지, 보안이나 데이터 무결성을 보장하는 장치가 아닙니다.

해결 방안

  1. Server Action 내부에서 항상 서버 측 검증을 다시 수행합니다 — 클라이언트 검증과 별개로, 서버가 신뢰할 수 있는 유일한 검증 지점입니다.
"use server";
import { z } from "zod";

const signUpSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

export async function signUpAction(formData: FormData) {
  const parsed = signUpSchema.safeParse({
    email: formData.get("email"),
    password: formData.get("password"),
  });
  if (!parsed.success) {
    return { error: parsed.error.issues[0]?.message };
  }
  // 검증된 parsed.data만 사용
}
  1. HTML 속성(required, type="email")은 그대로 유지합니다 — 없애라는 뜻이 아니라, "이것만으로는 부족하다"는 것이 핵심입니다. 정상적인 사용자에게는 즉각적인 피드백을 주는 역할을 여전히 합니다.
  2. 폼 필드로 넘어오는 id(게시글 id, 사용자 id 등)도 같은 원칙이 적용됩니다 — 폼 필드는 클라이언트가 조작할 수 있는 외부 입력이므로, 서버에서 그 id로 실제 존재하는 레코드인지, 요청한 사용자가 그 레코드에 접근/수정 권한이 있는지를 다시 확인해야 합니다.
  3. 검증 스키마를 클라이언트(폼의 즉각적 피드백용)와 서버(진짜 검증) 양쪽에서 같은 라이브러리(Zod 등)로 공유하면, 두 곳의 규칙이 어긋나는 것을 막으면서도 서버 검증을 생략하지 않을 수 있습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.