익명 폼 — 최소한의 안전망
익명 폼 — 최소한의 안전망
문의하기 · 댓글 · 건의함처럼 로그인 없이 공개된 폼은 가장 자주 남용당하는 표적입니다. CAPTCHA 를 안 쓰면 안 된다는 결론으로 건너뛰기 전에, 더 적은 비용으로 상당한 방어가 가능한 수단들이 있습니다.
1. 위협 모델
어떤 공격이 실제로 오는지 구분해 두면 방어 선택이 쉬워집니다.
| 위협 | 특징 | 비용 |
|---|---|---|
| 스팸 봇 | 양식을 HTML 파싱 후 자동 submit. IP 도 많이 씀 | 가장 흔함 |
| 수동 스팸 | 사람이 복붙 반복. IP 는 소수 | 중간 |
| 대상 공격 | 특정 사업체 허위 신고 · 조작 제보 | 낮음 · 고비용 |
| 시스템 낭비 | 리소스 고갈 (수천 / 초) | 낮음 · 드문 |
공개 문의 폼은 보통 1 번이 90 %. 이 1 번을 막으면 나머지는 운영적 · 수동 대응으로 감당 가능.
2. Honey-pot — 0 UX 비용의 1 차 방어
사람 눈에 안 보이는 필드를 폼에 숨기고, 값이 채워져 들어오면 봇으로 간주.
<form action={submit}>
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
style={{ position: 'absolute', left: '-9999px', opacity: 0 }}
/>
{/* 실제 필드들 */}
</form>
서버에서는:
export async function POST(req: Request) {
const body = await req.json();
if (body.website && body.website.trim()) {
return NextResponse.json({ ok: true }, { status: 200 });
// 일부러 200 — 봇이 "실패" 를 학습해 재시도하지 않게
}
// 정상 경로
}
사람 사용자는 보이지 않는 필드를 채우지 않으므로 UX 비용 0. 봇은 HTML 파싱만 하고 CSS 를 읽지 않는 경우가 많아 이 함정에 잘 걸립니다.
3. IP 해싱 — 원문 없이 중복 판별
request.headers['x-forwarded-for'] 같은 IP 를 원문 저장하면 개인정보 보관 의무가 생깁니다. 중복 · 남용 판별만 필요하면 해시 후 앞자리만 저장.
import { createHash } from 'crypto';
function hashIp(ip: string): string {
return createHash('sha256').update(ip).digest('hex').slice(0, 32);
}
await db.query(
'INSERT INTO inquiries (..., ip_hash) VALUES (..., $1)',
[hashIp(ip)]
);
- 같은 IP 의 반복 submit 은
WHERE ip_hash = ... AND created_at > now() - interval '1 hour'로 조회 가능 - 원문 IP 는 DB · 로그 어디에도 남지 않음
- 앞자리 32 자 truncate 는 저장 공간 · 충돌 확률의 현실적 절충
X-Forwarded-For 는 맨 앞이 원 IP 이지만 프록시 체인을 고려해 split(',')[0].trim() 으로 추출.
4. 속도 제한 — 운영 부담을 줄이는 두 층
로그인 없는 폼에 속도 제한을 적용하려면 IP 단위.
// Redis 기반 sliding window (권장)
await redis.incr(`inquiries:${ipHash}:${Math.floor(Date.now() / 60000)}`);
// 분당 3 회 제한 같은 식
Redis 가 무거우면 PostgreSQL 로도 가능 (SELECT count(*) FROM inquiries WHERE ip_hash=$1 AND created_at > now() - interval '1 minute'). DB 쓰기 횟수가 같지만 의존성이 줄어듭니다.
5. 입력 검증 — zod · Valibot
폼 필드는 서버에서 다시 검증. 클라이언트 검증은 UX 용, 보안 경계는 서버.
import { z } from 'zod';
const InquirySchema = z.object({
name: z.string().max(40).optional(),
email: z.string().email().max(120).optional(),
subject: z.string().max(80).optional(),
message: z.string().min(5).max(2000),
website: z.string().optional(), // honey-pot
});
const parsed = InquirySchema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: 'invalid' }, { status: 400 });
길이 상한 (max) 이 중요합니다. 메가바이트 단위 payload 로 DB · 메모리를 낭비하는 공격을 저비용으로 막습니다.
6. XSS — 저장 시점이 아니라 표시 시점
message 본문은 그대로 저장하고 표시할 때 sanitize. DB 에 HTML 을 저장하면 "원문 보존 · 재편집" 이 곤란.
import DOMPurify from 'isomorphic-dompurify';
const safe = DOMPurify.sanitize(marked.parse(message));
<div dangerouslySetInnerHTML={{ __html: safe }} />
마크다운 허용 폼이면 DOMPurify. 순수 텍스트면 React 의 자동 이스케이핑이 이미 충분.
7. 이메일 · 이름 등 PII 선택 저장
CREATE TABLE inquiries (
id BIGSERIAL PRIMARY KEY,
name TEXT, -- NULL 허용
email TEXT, -- NULL 허용
subject TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
user_agent TEXT,
ip_hash TEXT,
status VARCHAR(12) NOT NULL DEFAULT 'new'
CHECK (status IN ('new','read','replied','archived')),
admin_note TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
email을 필수로 두면 가짜 이메일이 많아져 오히려 노이즈.회신 받고 싶으면 입력로 선택 유지.user_agent는 500 자 truncate (봇은 종종 터무니없이 긴 UA 를 씀).status는new → read → replied → archived4 단계 flow 만으로도 운영자가 트래킹 가능.
8. 운영자 답신 동선
수신 폼만 있고 답신 경로가 없으면 운영자가 피로해집니다.
- 이메일 입력이 있으면 관리자 UI 에서
mailto:${email}?subject=Re: ...&body=...링크 status가replied로 바뀌면 해당 inquiry 는 대시보드에서 숨김archived는 법적 보관 기간 (예: 3 년) 후 배치로DELETE또는email=NULL익명화
9. CAPTCHA 가 필요해지는 시점
위 다섯 (honey-pot · IP 해시 · rate limit · zod · XSS 방어) 로 대부분의 소규모 사이트는 버팁니다. 다음 중 하나가 현실이 되면 CAPTCHA 를 추가.
- 하루 수십 건 이상의 스팸이 꾸준히
- 타겟팅 공격 (경쟁사 · 특정 개인의 반복 허위 제보)
- 결제 · 쿠폰 발급 같은 금전 가치가 있는 액션
hCaptcha · Cloudflare Turnstile 이 reCAPTCHA 보다 UX · 프라이버시가 낫다는 평가가 일반적.
10. 자주 걸리는 자리
CAPTCHA 먼저 도입 — UX 비용이 크고 봇은 CAPTCHA 도 돈 주고 뚫는 서비스가 있음. 먼저 honey-pot + rate limit 로 80 % 를 막고 후속 도입 판단.
IP 원문 저장 — GDPR · 한국 개인정보보호법의 개인식별정보 범주. 보유 기간 · 삭제 정책이 필요. 해싱은 이를 회피.
실패 시 4xx 반환 — 봇이 "이 폼은 안 되는구나" 를 학습해 다른 폼으로 이동하거나 우회합니다. honey-pot 걸린 봇에는 200 OK + 실제 저장 안 함 이 심리전 관점에서 유리.
user_agent 검증으로 봇 차단 — 정상 브라우저 UA 를 위장한 봇이 많아 효과가 미미. 차단 로직보다는 로깅 용도.
관리자 UI 에 raw message 직접 렌더 — admin 페이지라고 XSS 안전하지 않습니다. 관리자 브라우저 탈취가 타깃인 공격도 존재.
하고픈 말
공개 폼의 보안은 CAPTCHA 같은 정면 방어보다 다층 저비용 방어의 합이 대체로 더 효과적입니다. honey-pot 하나만으로도 첫 해는 버틸 수 있고, 운영 로그에서 실제 위협 패턴이 보이고 나서 더 무거운 수단을 추가하는 순서가 아프지 않습니다.
Next
- input-validation-zod
- rate-limit-redis
OWASP Automated Threats Handbook · Cloudflare Turnstile · DOMPurify · RFC 7239 X-Forwarded-For · 개인정보보호법 을 참고합니다.