ATM·키오스크 프론트 입력 유효성 정규식 25개 — 전부 돌려본 통과/거절 케이스와 함께
어제 글(유효성 검사 실전 시나리오 30가지)이 서버 쪽 이야기였다면, 이번에는 화면 쪽이다. ATM·은행 키오스크·카드 등록 화면처럼 숫자 키패드 중심의 금융 입력 UI에서 쓰는 정규식 25개를 정리했다.
먼저 원칙 세 가지를 짚는다.
- 프론트 정규식은 UX이지 보안이 아니다. OWASP는 유효성 검사를 “형식(syntax)과 의미(semantics)” 두 층으로 나누고, 유효한 계좌번호라고 해서 그 계좌에 접근할 권한이 있는 건 아니라고 못박는다(OWASP Input Validation Cheat Sheet). 같은 검사를 서버에서 반드시 다시 한다.
- 전체 일치(full match)로 쓴다. 앞뒤에
^…$가 없으면abc1234xyz안의1234가 통과한다. HTMLpattern속성은 암묵적으로^(?:…)$로 감싸 준다(MDNpattern). - “거절용” 정규식과 “허용용” 정규식을 구분한다. 아래 03~06번은 걸리면 막는 정규식이다. 이걸 허용 패턴 자리에 넣으면 정반대로 동작한다.
검증 방법. 25개 전부를 Node.js 24.14.0 에서 통과 케이스와 거절 케이스로 돌렸다. HTML
pattern과 같은 조건(v플래그,^(?:…)$감싸기)으로도 따로 컴파일해서 돌렸다. 결과는 실패 0건이다. 5,000자짜리 near-miss 입력(111…1x,aaa…a!등)으로 재본 백트래킹 시간도 전부 50ms 미만이었다. 표에 적은 케이스가 그대로 테스트 데이터다.
A. 인증 — PIN
01. PIN 4자리
^\d{4}$
| 통과 | 거절 |
|---|---|
0000 1234 |
123 12345 12a4 1234(전각) ` 1234`(앞 공백) |
JavaScript의 \d 는 u·v 플래그를 켜도 ASCII 0-9 만 뜻한다. 그래서 전각 숫자 1234 는 거절된다. 이게 버그인지 기능인지는 아래 「입력 위생」 절에서 다룬다.
02. PIN 4~12자리 (국제 표준 범위)
^\d{4,12}$
| 통과 | 거절 |
|---|---|
1234 123456789012 |
123 1234567890123 |
ISO 9564 계열의 PIN 블록 포맷은 PIN 길이 필드에 4~12만 허용한다(ISO 9564-1:2011/Amd 1:2015 샘플, 9.4.2). 국내 카드·계좌 비밀번호는 4자리가 일반적이다. 그래도 해외 카드를 받는 단말이라면 상한을 4로 박지 않는 편이 안전하다.
03. 동일 숫자 반복 — 거절용
^(\d)\1+$
| 걸림(=거절) | 안 걸림 |
|---|---|
0000 777777 |
0001 1212 |
역참조 \1 로 “첫 숫자와 같은 숫자가 끝까지 반복”을 잡는다. NIST SP 800-63B는 사용자가 고른 비밀값을 흔한 값 목록과 대조하라고 권고하면서, 그 예로 “Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’)” 를 든다(NIST SP 800-63B §5.1.1.2).
04. 2자리 패턴 반복 — 거절용
^(\d{2})\1+$
| 걸림 | 안 걸림 |
|---|---|
1212 343434 |
1213 1221 |
05. 오름차순 연속 숫자 — 거절용
^(?:0(?=1)|1(?=2)|2(?=3)|3(?=4)|4(?=5)|5(?=6)|6(?=7)|7(?=8)|8(?=9))+\d$
| 걸림 | 안 걸림 |
|---|---|
1234 456789 0123 |
1235 4321 9012 |
1234|2345|… 처럼 나열하면 길이마다 목록이 늘어난다. 이 식은 전방탐색 (?=…)으로 “다음 글자가 나보다 1 큰가”만 본다. 그래서 4자리든 6자리든 식 하나로 끝난다. 9012 처럼 9에서 0으로 넘어가는 경우는 연속으로 보지 않는다. 이걸 막을지는 정책으로 정할 일이다.
06. 내림차순 연속 숫자 — 거절용
^(?:9(?=8)|8(?=7)|7(?=6)|6(?=5)|5(?=4)|4(?=3)|3(?=2)|2(?=1)|1(?=0))+\d$
| 걸림 | 안 걸림 |
|---|---|
4321 987654 3210 |
1234 4320 0987 |
생년월일·전화번호 뒷자리와 같은 PIN은 정규식으로 막을 문제가 아니다. 사용자 정보와 값을 비교해야 한다. 이 비교는 사용자 정보를 가진 서버에서 한다.
B. 계좌·카드
07. 계좌번호 — 숫자만
^\d{10,14}$
| 통과 | 거절 |
|---|---|
1234567890 12345678901234 |
123456789 123-456-7890 |
국내 계좌번호는 은행마다 자릿수와 하이픈 위치가 다르다. 그래서 자릿수 범위만 형식으로 보고, 실제 존재 여부는 예금주 조회 API에 맡긴다. 10~14는 이 글에서 가정한 범위다. 운영에서는 취급 은행 목록에 맞춰 정한다.
08. 계좌번호 — 하이픈 허용, 자릿수는 숫자만 셈
^(?=(?:-?\d){10,14}$)\d+(?:-\d+){0,3}$
| 통과 | 거절 |
|---|---|
110-123-456789 3333-01-2345678 1234567890 |
110--123-456789 -1101234567 110-123-456789- 1-2-3-4-5678901 110-12 |
식은 두 부분으로 되어 있다.
- 앞의 전방탐색: 하이픈을 뺀 숫자가 10~14개인지만 센다.
- 뒷부분: 하이픈이 연속되거나 맨 앞·맨 뒤에 오지 않는지, 3개 이하인지를 본다.
화면에서는 사용자가 하이픈을 넣든 말든 받아 준다. 서버로 보낼 때는 value.replace(/-/g, '') 로 정규화한 값을 보낸다.
09. 카드번호(PAN) — 12~19자리
^\d{12,19}$
| 통과 | 거절 |
|---|---|
4111111111111111(16) 378282246310005(15) |
41111111111(11) 41111111111111111111(20) |
ISO 9564 Format 4 PIN 블록의 PAN 필드는 PAN 길이를 12~19로 인코딩한다. 12자리보다 짧으면 0으로 채운다(ISO 9564-1 Amd 1 샘플, 9.4.2.2.3). PAN 번호 체계 자체는 ISO/IEC 7812-1이 정한다(ISO/IEC 7812-1:2017). Luhn 체크섬은 정규식으로 할 수 없다. 자릿수가 맞는지만 정규식으로 보고, 체크섬은 별도 함수로 계산한다.
10. 카드번호 — 4-4-4-4 표시, 구분자는 하나로 통일
^\d{4}([ \-]?)\d{4}\1\d{4}\1\d{4}$
| 통과 | 거절 |
|---|---|
4111 1111 1111 1111 4111-1111-1111-1111 4111111111111111 |
4111-1111 1111-1111(섞임) 4111 11111111 1111 |
첫 구분자를 그룹 ([ \-]?) 으로 잡고, 나머지 자리에서는 \1 로 같은 구분자만 허용한다. 공백·하이픈·없음 중 하나를 고르면 끝까지 그것만 써야 한다. 15자리 Amex(4-6-5)처럼 그룹 구성이 다른 카드는 따로 식을 만든다.
- 를 \- 로 이스케이프한 이유가 있다. 이 식을 pattern 속성에 넣으면 v 플래그로 컴파일되는데, v 모드에서는 문자 클래스 안의 - 를 이스케이프해야 한다(MDN pattern).
11. 카드 유효기간 MM/YY
^(0[1-9]|1[0-2])\/?(\d{2})$
| 통과 | 거절 |
|---|---|
01/27 1229 09/30 |
00/27 13/27 1/27 12-27 |
형식만 본다. “이미 지난 달인가”는 의미 검사다. 캡처한 월·연도로 Date 를 만들어 비교한다.
12. CVC/CVV 3~4자리
^\d{3,4}$
| 통과 | 거절 |
|---|---|
123 1234 |
12 12345 |
카드사에 따라 3자리인지 4자리인지가 달라진다(Amex는 4자리). 그래서 PAN 앞자리로 브랜드를 판별한 뒤 ^\d{3}$ 이나 ^\d{4}$ 로 좁히는 편이 더 정확하다.
13. 마스킹된 카드번호 표시값 — 노출 자릿수 상한
^\d{6}\*{2,9}\d{4}$
| 통과 | 거절 |
|---|---|
411111******1111 378282*****0005 |
4111111111111111(마스킹 안 됨) 4111********1111(BIN 4자리) 411111******11111(끝 5자리) |
입력을 검사하는 식이 아니다. 화면에 띄우기 직전의 표시값을 검사하는 단언(assertion) 이다. PCI DSS v4.0 요구사항 3.4.1은 PAN을 표시할 때 “BIN과 마지막 4자리가 표시 가능한 최대치”라고 정한다. 그 이상을 볼 수 있는 건 업무상 필요가 문서화된 역할뿐이다(PCI DSS v4.0 SAQ B-IP, 3.4.1). 개발 모드에서 렌더 직전에 이 식으로 assert 를 걸면, 마스킹이 빠진 화면이 배포되는 사고를 테스트 단계에서 잡을 수 있다. BIN을 8자리로 쓰는 체계라면 \d{6} 을 그에 맞춰 바꾼다.
C. 금액
14. 금액 — 원 단위 정수, 앞자리 0 금지
^(?:0|[1-9]\d{0,8})$
| 통과 | 거절 |
|---|---|
0 50000 999999999 |
007 1000000000 -1 1e5 10.5 |
Number("1e5") 는 100000 이고 Number("007") 은 7 이다. 숫자로 바꾼 뒤에 검사하면 이런 표기가 다 통과해 버린다. 그래서 문자열 단계에서 먼저 막는다. 상한(9자리)은 자릿수로만 정했다. 실제 이체·출금 한도는 서버의 정책값으로 다시 확인한다.
15. 금액 — 만원 단위 (ATM 현금 출금 화면)
^[1-9]\d{0,2}0000$
| 통과 | 거절 |
|---|---|
10000 500000 9990000 |
0 15000 10000000 0010000 |
ATM 출금처럼 권종 단위로만 내줄 수 있는 화면에서 쓴다. 단위와 상한(이 식은 999만 원)은 그 기기의 권종 구성과 한도 정책을 반영한 가정값이다. 상수로 하드코딩하지 말고 설정에서 정규식을 만들어 쓰는 편이 낫다.
16. 금액 — 천 단위 콤마가 찍힌 표시값
^(?:0|[1-9]\d{0,2}(?:,\d{3})*)$
| 통과 | 거절 |
|---|---|
0 1,000 12,345,678 |
1000,000 1,00 01,000 ,000 1,000, |
(?:,\d{3})* 는 반복 그룹이다. 하지만 그룹이 리터럴 , 로 시작하므로 입력의 어느 위치에서 그룹이 시작할지 갈래가 생기지 않는다. OWASP가 ReDoS 위험으로 꼽는 “겹치는 반복의 반복”((a+)+)과는 구조가 다르다(OWASP ReDoS). 그래도 정규식에 넣기 전에 길이부터 자른다(maxlength).
17. 외화 금액 — 소수 2자리까지
^(?:0|[1-9]\d{0,6})(?:\.\d{1,2})?$
| 통과 | 거절 |
|---|---|
0.5 100 1234.56 |
.5 100. 1.234 01.5 |
통화마다 소수 자릿수가 다르다. 원화처럼 0자리인 통화도 있다. 그래서 \d{1,2} 의 상한을 통화 코드에 맞춰 바꾼다. 검증을 통과한 값은 parseFloat 에 넣지 말고 최소 단위 정수(센트 등)로 바꿔서 다룬다.
D. 신원·연락처
18. 받는 분 통장 표시 — 완성형 한글·영숫자 7자
^[가-힣A-Za-z0-9]{1,7}$
| 통과 | 거절 |
|---|---|
홍길동 월세10월 ABC123 |
ㅎㄱㄷ(자모만) 홍 길동(공백) 홍길동월세보냄요(8자) 홍길동! |
[가-힣] 은 완성형 음절만 받는다. 한글 IME가 조합 중인 순간에는 input 이벤트의 값이 ㅎ·호 처럼 미완성일 수 있다. 그래서 이 검사는 input 이벤트마다 하지 말고 compositionend 이후나 blur 시점에 한다. 길이 7은 이 글에서 정한 예시 정책이다.
19. 휴대폰 번호 010 — 구분자 통일
^010([\- ]?)\d{4}\1\d{4}$
| 통과 | 거절 |
|---|---|
010-1234-5678 01012345678 010 1234 5678 |
010-1234 5678(섞임) 011-123-4567 010-123-4567 |
10번과 같은 역참조 기법이다. 01X 번호나 가운데 3자리 번호까지 받아야 하는 서비스라면 ^01[016789]([\- ]?)\d{3,4}\1\d{4}$ 처럼 넓힌다. 어디까지 받을지는 서비스 정책이다.
20. 주민등록번호 — 형식만 보고, 체크섬은 보지 않는다
^\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])-?[1-8]\d{6}$
| 통과 | 거절 |
|---|---|
900101-1234567 0512314123456 |
901301-1234567(13월) 900132-1234567(32일) 900101-9234567 900101-123456 |
이 25개 중 가장 많이 틀리는 항목이다. 옛 주민등록번호는 뒷자리 마지막 숫자가 검증번호였다. 그래서 인터넷에 도는 검증 코드 상당수가 가중합 체크섬을 계산한다. 그런데 2020년 10월부터 새로 부여되거나 변경되는 번호는 뒷자리 중 성별 자리를 뺀 6자리가 임의번호다(행정안전부 보도자료, 2019.12.17, 정책브리핑, 2020.05.25). 체크섬 검사를 남겨 두면 2020년 10월 이후 번호를 받은 출생자·번호 변경자를 정상 번호인데도 거절하게 된다.
[1-8] 은 내국인(1~4)과 외국인등록번호(5~8)의 성별·세기 자리를 받는다. 1800년대 출생(9·0)은 뺐다. 그리고 주민번호는 화면에 남기지 않는 것이 먼저다. 입력 필드에 autocomplete="off" 를 걸고, 뒷자리는 마스킹해서 표시한다.
21. 생년월일 YYYYMMDD
^(?:19|20)\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])$
| 통과 | 거절 |
|---|---|
19900101 20051231 |
18991231 19901301 1990-01-01 |
20230231(2월 31일)은 통과한다. 정규식으로 윤년과 월별 일수까지 표현할 수는 있지만 식을 읽을 수 없게 된다. 날짜로 만든 뒤 연·월·일이 그대로 나오는지 되읽어 확인한다.
const ok = (y, m, d) => { const t = new Date(Date.UTC(y, m - 1, d));
return t.getUTCFullYear() === y && t.getUTCMonth() === m - 1 && t.getUTCDate() === d; };
22. 문자 인증번호 6자리
^\d{6}$
| 통과 | 거절 |
|---|---|
012345 |
12345 1234567 12 345 |
식보다 속성이 중요하다. <input inputmode="numeric" autocomplete="one-time-code"> 로 두면 모바일 키보드가 문자로 받은 코드를 제안해 준다. one-time-code 는 HTML 표준의 autofill 토큰이다(WHATWG HTML, autofill). type="number" 는 쓰지 않는다. 숫자로 해석되면서 012345 의 앞자리 0이 사라진다.
23. 이메일 (영수증 발송) — HTML 표준의 식, 단 그대로 붙이면 안 된다
^[a-zA-Z0-9.!#$%&'*+\/=?^_`\{\|\}~\-]+@[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$
| 통과 | 거절 |
|---|---|
a@b.co first.last+tag@sub.example.kr user@localhost a@b x{y}~z@q.kr |
@b.co a b@c.kr a@-b.kr a@b..kr 홍@b.kr |
이 식은 WHATWG HTML 표준이 type="email" 의 “valid e-mail address”로 제시하는 정규식이다(WHATWG HTML, input type=email). 다만 한 군데를 고쳤다. 표준 원문의 문자 클래스는 [a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-] 이다. 이걸 그대로 pattern 속성에 넣으면 v 플래그 컴파일에서 Invalid character in character class 오류가 난다. 직접 확인한 결과다. u 플래그로는 컴파일된다. MDN에 따르면 pattern 은 값이 잘못된 정규식이면 아예 적용되지 않는다. 오류도 보이지 않고 아무 값이나 통과한다(MDN pattern). v 모드에서 이스케이프가 필요한 { | } - 에 백슬래시를 붙인 것이 위의 식이다.
두 가지를 더 짚는다.
- 이 식은
a@b처럼 점 없는 도메인도 통과시킨다. 표준이 의도한 동작이다. - 사실
type="email"을 쓰면 브라우저가 이 검사를 대신 한다. 정규식을 직접 들고 다닐 이유는 서버 검증과 규칙을 맞출 때뿐이다.
24. 영문 명의 (카드 소지자명)
^(?=.{2,26}$)[A-Z]+(?: [A-Z]+)*$
| 통과 | 거절 |
|---|---|
HONG GILDONG KIM A |
hong gildong HONG GILDONG(공백 2개) ` HONG HONG-GILDONG` |
(?: [A-Z]+)* 는 반드시 공백 하나로 시작하므로 갈래가 겹치지 않는다. 그래도 길이 상한은 전방탐색으로 먼저 건다. 단, 이 식은 카드 엠보싱 표기처럼 형식이 정해진 칸에만 쓴다. 일반 이름 칸에 쓰면 하이픈·아포스트로피가 들어간 실제 이름을 막는다. OWASP는 바로 이런 차단을 피하라고 예를 든다(아포스트로피를 막으면 정당한 이름이 거절된다)(OWASP Input Validation Cheat Sheet).
25. 키패드 입력 “중간 상태” 필터
^\d{0,6}$
| 통과 | 거절 |
|---|---|
(빈 값) 1 123456 |
1234567 12a |
나머지 24개는 제출 시점의 최종값을 검사하는 식이다. 이 식만 타이핑 중에 쓴다. 입력 중에는 12 처럼 아직 덜 친 값이 정상이다. 그래서 최종 규칙(^\d{6}$)으로 input 이벤트를 막으면 사용자는 첫 글자부터 빨간 테두리를 보게 된다. 정리하면 다음과 같다.
- 입력 중에는 앞으로 완성될 수 있는 값(prefix)을 허용하는 식을 쓴다.
- 제출할 때는 최종 식을 쓴다.
prefix 식은 {n} 을 {0,n} 으로 바꾸는 식으로 대개 기계적으로 만들 수 있다.
입력 위생 — 정규식 앞단에서 할 일
정규식이 같아도 무엇을 넣느냐에 따라 결과가 달라진다. 직접 돌려 본 결과는 다음과 같다.
| 입력 | /^\d{4}$/ 결과 |
원인 |
|---|---|---|
1234 (전각) |
false | \d 는 ASCII 전용. NFKC 정규화 후에는 true |
"12\u200B34" (사이에 U+200B) |
false | 눈에 안 보이는 제로폭 공백 |
| ` 1234` (붙여넣기 공백) | false | 앞뒤 공백 |
전각 숫자는 한·중·일 IME에서 실제로 들어오는 값이다. 막을지 받을지 정해야 한다. 받기로 했다면 정규식을 검사하기 전에 value.normalize('NFKC').trim() 을 거친다. 다만 Unicode 표준(UAX #15)은 호환 정규화(NFKC)가 의미 있는 구분까지 지울 수 있다고 경고한다(UAX #15). 그래서 숫자 전용 칸처럼 허용 문자가 좁은 필드에만 적용한다. OWASP도 정규화는 검증·저장·비교 전에 같은 정책으로 적용하라고 하고, 정규화가 새니타이즈를 대신하지는 않는다고 덧붙인다(OWASP Input Validation Cheat Sheet).
그 밖에 지킬 것:
- 길이 먼저, 정규식은 그다음.
maxlength와 길이 비교로 입력을 자른 뒤 정규식을 돌린다. OWASP 권고 순서도 같다. type="number"대신inputmode="numeric"을 쓴다. PIN·계좌·카드·인증번호는 숫자 모양의 문자열이다.number타입은 앞자리 0을 없애고e를 허용하며, 휠 스크롤로 값이 바뀐다.pattern에는title과 눈에 보이는 안내문을 함께 둔다. 형식을 툴팁으로만 알려주면 키보드·터치 사용자는 보지 못한다(MDNpattern).- 사용자 입력으로 정규식을 만들지 않는다.
new RegExp(userInput)은 정규식 주입이자 ReDoS 통로다(OWASP ReDoS).
한 장 요약
| # | 용도 | 핵심 |
|---|---|---|
| 01–02 | PIN | 4자리 고정 / 국제 4~12 |
| 03–06 | 약한 PIN | 거절용. 반복·2자리 패턴·오름·내림 |
| 07–08 | 계좌 | 자릿수만 보고, 하이픈은 정규화 |
| 09–13 | 카드 | 자릿수 → Luhn은 코드로, 구분자 통일, 표시값 마스킹 단언 |
| 14–17 | 금액 | 문자열 단계에서 007·1e5 차단, 단위·소수 자릿수는 설정값 |
| 18–24 | 신원 | 주민번호 체크섬 금지(2020.10~), 날짜는 되읽기, 이메일은 v 이스케이프 |
| 25 | 입력 중 | prefix 식과 최종 식 분리 |
References
- OWASP, Input Validation Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- OWASP (Adar Weidman 외), Regular expression Denial of Service – ReDoS. https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- MDN Web Docs, HTML attribute: pattern. https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/pattern
- WHATWG, HTML Living Standard — 4.10.5 The input element (valid e-mail address). https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
- WHATWG, HTML Living Standard — Autofill (
one-time-code). https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill - ISO, ISO 9564-1:2017 Financial services — PIN management and security — Part 1. https://www.iso.org/standard/68669.html
- ISO, ISO 9564-1:2011/Amd 1:2015 (샘플 페이지, Format 4 PIN 길이·PAN 길이 필드). https://cdn.standards.iteh.ai/samples/61447/358d47ddfa8e4130bd29aadee6725f67/ISO-9564-1-2011-Amd-1-2015.pdf
- ISO/IEC, ISO/IEC 7812-1:2017 Identification cards — Identification of issuers — Part 1: Numbering system. https://www.iso.org/standard/70484.html
- PCI Security Standards Council, PCI DSS v4.0 SAQ B-IP (Requirement 3.4.1). https://listings.pcisecuritystandards.org/documents/PCI-DSS-v4-0-SAQ-B-IP.pdf
- NIST, SP 800-63B Digital Identity Guidelines: Authentication and Lifecycle Management (§5.1.1.2). https://pages.nist.gov/800-63-3/sp800-63b.html
- 행정안전부, 「45년만에 주민등록번호 부여체계 바꾼다」 보도자료 (2019.12.17). https://www.mois.go.kr/frt/bbs/type010/commonSelectBoardArticle.do?bbsId=BBSMSTR_000000000008&nttId=74731
- 대한민국 정책브리핑, 「주민번호 뒷자리 지역표시번호 10월부터 폐지…’45년만에 개편’」 (2020.05.25). https://www.korea.kr/news/policyNewsView.do?newsId=148872723
- Unicode, UAX #15: Unicode Normalization Forms. https://unicode.org/reports/tr15/