Files
2nd/10_Wiki/Topics/Coding/Security_Input_Validation.md
T
2026-05-09 21:08:02 +09:00

4.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
security-input-validation 입력 검증 — 경계에서 정규화 + 도메인 타입화 Coding draft B conceptual 2026-05-09 2026-05-09
security
validation
zod
vibe-coding
language applicable_to
TypeScript / zod
Backend
Web
zod
schema validation
parse don't validate
sanitize

입력 검증

"Parse, don't validate". 경계에서 schema → 도메인 타입 으로 변환. 이후 코드는 안전 가정. 클라이언트 검증은 UX 만, 서버에서 항상 재검증.

📖 핵심 개념

  • 모든 외부 입력 = 적. (HTTP body, query, headers, env, file, 다른 서비스 응답)
  • 검증 = "조건 통과"가 아니라 "정확한 타입으로 변환".
  • 검증 후 타입 = brand 또는 nominal.

💻 코드 패턴

Zod schema (Express)

import { z } from 'zod';

const SignupSchema = z.object({
  email: z.string().email().toLowerCase().max(254),
  password: z.string().min(8).max(128),
  age: z.coerce.number().int().min(13).max(120).optional(),
  inviteCode: z.string().regex(/^[A-Z0-9]{6,8}$/).optional(),
}).strict(); // 추가 키 거부

type Signup = z.infer<typeof SignupSchema>;

app.post('/api/signup', async (req, res) => {
  const parsed = SignupSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(422).json({ error: 'validation', issues: parsed.error.flatten() });
  }
  const data: Signup = parsed.data; // 타입 안전, 정규화 됨
  await createUser(data);
  res.status(201).end();
});

Query / params 검증

const ListUsersQ = z.object({
  page: z.coerce.number().int().positive().default(1),
  pageSize: z.coerce.number().int().min(1).max(100).default(20),
  q: z.string().max(200).optional(),
});

app.get('/api/users', (req, res) => {
  const q = ListUsersQ.parse(req.query);
  // q.page is number now, not string
});

Env 검증 — 부팅 시

const Env = z.object({
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  NODE_ENV: z.enum(['development', 'production', 'test']),
  PORT: z.coerce.number().default(3000),
});
export const env = Env.parse(process.env); // 부팅 시 fail-fast

외부 API 응답 — 신뢰하지 마라

const StripeCustomer = z.object({
  id: z.string().startsWith('cus_'),
  email: z.string().email().nullable(),
  metadata: z.record(z.string()).default({}),
});

const customer = StripeCustomer.parse(await stripe.customers.retrieve(id));
// 외부 API 가 형식 바뀌면 즉시 알림

Brand 결합

const Email = z.string().email().toLowerCase().brand<'Email'>();
type Email = z.infer<typeof Email>;

function send(to: Email, body: string) { ... }
const e: Email = Email.parse(req.body.email);
send(e, '...'); // 검증된 Email 만 통과

🤔 의사결정 기준

입력 종류 위치
HTTP body / query / params route handler 입구
환경 변수 부팅 시점 (실패 즉시 종료)
파일 업로드 (mime, size) 업로드 미들웨어
WebSocket 메시지 메시지 라우터 입구
외부 API 응답 client wrapper
큐 메시지 worker 입구

안티패턴

  • req.body.x 그대로 사용: 형식 / 타입 / 길이 보장 X.
  • 클라이언트 검증만: 우회 가능. 서버 = 진실.
  • as Type 캐스팅: 검증 없음. parse 사용.
  • regex 만으로 이메일 검증: 너무 엄격 / 느슨. zod email 사용.
  • schema 가 너무 느슨 (z.any()): 의미 없음.
  • error 메시지에 입력 그대로 echo: XSS. 일반 메시지.
  • HTML / SQL 검증 없이 통과: 별도 escape 단계 (output encoding) 도 필요.
  • strict() 안 씀: 추가 필드 무시 — 클라이언트 의도 파악 어려움 / 잠재 보안 사고.

🤖 LLM 활용 힌트

  • 새 endpoint: zod schema 먼저 작성, 핸들러는 parse 결과 사용.
  • env 도 zod parse — 부팅 시 fail-fast.
  • 외부 API 응답도 schema.

🔗 관련 문서