UX
웹 폼 오류 메시지에 내부 정보를 노출했던 실수와 수정 기준
사용자에게 보여 줄 오류와 서버 로그에 남길 세부 정보를 분리하지 않아 생긴 보안 실수와 수정 기준을 적었습니다.
친절하게 보여 주려던 문자열
문의 폼에서 저장이 실패하면 사용자가 다시 시도할 수 있게 하려고 서버의 오류 메시지를 그대로 화면에 넣었다. 개발 환경에서는 “database connection refused”가 유용했지만, 운영에서 테이블 이름과 내부 호스트명이 함께 노출됐다. 에러를 숨긴 것이 문제가 아니라, 같은 메시지를 두 대상에게 보낸 것이 문제였다.
외부 응답과 내부 기록을 분리했다
클라이언트에는 추적 가능한 짧은 코드만 반환하고, 서버 로그에는 원인과 request ID를 남기도록 바꿨다.
const requestId = crypto.randomUUID()
try {
await saveContact(input)
return Response.json({ ok: true }, { status: 201 })
} catch (error) {
console.error({ requestId, error })
return Response.json(
{ ok: false, code: 'CONTACT_SAVE_FAILED', requestId },
{ status: 503 }
)
}
request ID도 이메일, 비밀번호, 전체 요청 본문과 함께 기록하지 않도록 로거의 필드 필터를 확인했다. “로그에 남겼으니 괜찮다”가 아니라 로그 접근 권한과 보존 기간까지 운영 범위에 포함시켰다.
상태 코드도 다시 맞췄다
모든 실패를 500으로 보내면 사용자가 고칠 수 있는 입력 오류와 서버 장애를 구별하기 어렵다. 다음처럼 기준을 고정했다.
- 입력 검증 실패: 400, 필드별 안전한 메시지
- 인증 부족: 401 또는 403, 내부 이유는 숨김
- 일시적 저장소 장애: 503, 재시도 안내와 request ID
- 예상하지 못한 예외: 500, 일반 오류 문구
테스트에서 확인한 노출 경로
화면만 보지 않고 응답 JSON, 서버 로그, reverse proxy 로그를 함께 확인했다. SQL 오류, 파일 경로, 환경 변수 이름, 스택 트레이스가 각각 새어 나오지 않는지 문자열 패턴으로 테스트했다.
curl -i -X POST https://example.com/api/contact -H 'content-type: application/json' -d '{"email":"bad","message":"test"}'
마무리하면서 코드 리뷰 규칙에 한 줄을 추가했다. 외부 오류 응답은 계약이고, 내부 예외 객체는 관측 데이터다. 둘을 직접 연결하는 코드는 편하지만, 운영 환경에서는 그 편의가 가장 먼저 줄여야 할 정보 노출 면적이 된다.