4.1 KiB
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 |
|
|
|
입력 검증
"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.