닉네임에 특수문자를 쓸 수 있냐는 CS 문의가 자주 왔다. MemberProfileRequest 의 @Pattern 허용 목록을 보고 답했다. 콤마가 된다고 답했는데 실제로는 안 됐다.
Table of contents
Open Table of contents
증상 — 한 목록만 보고 답한 결과
내가 본 것은 @Pattern(regexp = "^[가-힣A-Za-z0-9 ,._-]+$") 에 나열된 허용 문자였다. 거기에 콤마가 들어 있었다. Bean Validation 2.0 명세대로 @Pattern 은 값이 그 정규식에 맞아야 통과하는 허용 목록 방식이다. 그 목록이 최종 판정이라고 전제한 것이 틀렸다.
한 곳만 보고 답하면 그 답이 맞는지 틀리는지도 알 수 없다. 통과 여부를 정하는 자리가 몇 개인지부터 확인했어야 했다.
원인 — 앞에 있던 다른 검증
필터 체인을 열어 보니 컨트롤러 앞에 InputSanitizeFilter 가 붙어 있었다. 보안 검토 때 XSS 와 SQL 주입 대비로 넣은 OncePerRequestFilter 였다. 금지 문자를 나열하는 방식이었고 거기에 콤마가 있었다.
요청은 두 단계를 순서대로 지난다. 앞의 금지 목록을 통과하고 뒤의 @Pattern 허용 목록에도 들어야 저장까지 간다.
성격이 정반대인 두 레이어
앞은 적혀 있으면 차단하고 뒤는 적혀 있어야 통과한다. 방식이 정반대라 한쪽 목록만 보면 반대 방향으로 틀린 답이 나온다.
실제로 어긋나는 것이 양쪽에 있었다. 뒤에서는 허용인데 앞에서 막히는 콤마가 있고, 앞은 통과인데 뒤의 목록에 없어서 막히는 @ 도 있었다.
조치 — 최종 허용은 교집합이다
두 가지 방법이 있었다. 하나는 두 목록을 하나로 합쳐 한 자리에서만 판정하게 고치는 것이다. 다른 하나는 둘을 그대로 두고 문자별로 앞의 판정과 뒤의 판정과 최종 결과를 세 칸으로 나눈 표를 만드는 것이다. 뒤쪽을 골랐다. 이유는 InputSanitizeFilter 가 보안 검토의 산출물이라 우리 팀이 임의로 없앨 수 없어서다. 대신 문자가 추가될 때마다 표를 갱신해야 하는 것을 감수했다.
최종 허용 문자는 앞의 금지 목록에 없으면서 뒤의 허용 목록에 있는 것이었다. 두 칸만 있으면 되는지 안 되는지는 알아도 왜 안 되는지를 말할 수 없다. 어느 레이어에서 막혔는지가 세 번째 칸에서 나온다.
조건부 생략과 영역별 규칙
여기에 예외가 하나 더 있었다. shouldNotFilter() 가 /api/admin/** 에서 참을 돌려줘 앞의 검증이 통째로 생략됐다. 그러면 앞에서 막히던 문자가 관리자 경로에서만 허용되므로 같은 문자가 조건에 따라 되기도 안 되기도 한다.
필드마다 붙는 @Pattern 도 달라서 닉네임에서는 허용되는 문자가 메모 필드에서는 금지였다. 정규식만 읽으면 그것이 어느 필드에 붙는지 모르므로 DTO 의 선언 지점까지 봐야 표가 완성됐다.
정리
- 검증 레이어가 둘 이상일 수 있다
- 한 곳만 보고 답하면 그 답의 진위도 알 수 없다
- 성격이 반대인 두 레이어면 한쪽만 보고 답하면 틀린다
- 최종 허용은 두 판정의 교집합이다
- 어긋나는 문자가 양방향으로 있다
- 표에 각 레이어의 판정과 최종을 나눠 적어야 이유가 나온다
shouldNotFilter()처럼 조건에 따라 한 레이어가 통째로 생략될 수 있다- 정규식만 보지 말고
@Pattern이 붙은 필드까지 본다