입력 타입과 크기 검증
65분 안팎
학습 목표
허용 형식과 길이의 경계값을 검증합니다.
개념
받아들일 값의 계약을 먼저 정합니다
개인 자료 보관 앱은 자료 제목과 본문을 받아 저장합니다. 로그인과 소유자 확인이 끝났어도 입력이 앱이 다룰 수 있는 형태라는 보장은 없습니다. 숫자 제목이나 매우 큰 본문을 그대로 받아들이면 뒤 단계에서 예외가 나거나 자원을 과하게 씁니다. 입력 검증은 업무가 허용한 데이터를 고르는 작업입니다. SQL 질의 구조 보호와 화면 출력 보호는 각각 다른 단계에서 수행하며 제목 검사가 둘을 대신하지 않습니다.
실습은 본인 소유 환경의 합성 자료만 다룹니다. 브라우저 실습은 표준 입력 JSON 한 개와 판정 한 줄을 사용합니다. 로컬 ZIP은 Node.js와 Python 3 표준 모듈로 실행하며 ZIP 루트에서 bash check.sh를 입력합니다. 미션은 앞 모듈 solution의 앱·보고서를 이어받습니다. Docker로 이어지는 기존 검사는 legacy-check.sh로 보존하고 external 대기로 표시합니다. 이번 로컬 검사는 포트를 열지 않으며 실제 사용자 자료와 비밀번호를 입력하지 않습니다.
형식 검사를 값 검사보다 먼저 둡니다
이번 계약은 최상위 JSON 객체에 title과 content만 허용합니다. 배열·null·숫자는 객체로 인정하지 않습니다. title과 content는 둘 다 문자열이어야 하며 누락도 거절합니다. title은 원문 기준 80 코드 포인트 이하이고 trim한 결과가 비어 있지 않아야 합니다. content는 원문 기준 2048 코드 포인트 이하이며 빈 문자열을 허용합니다. 알 수 없는 필드는 거절하므로 owner나 role을 추가해 소유자나 역할을 주장할 수 없습니다.
JavaScript에서 typeof null은 object이며 배열도 object입니다. 따라서 값의 존재, typeof 결과, Array.isArray를 함께 확인합니다. 문자열임을 확인하기 전에 trim을 호출하면 숫자나 누락 필드에서 TypeError가 납니다. 검사는 유형, 허용 키, 문자열 유형, 길이와 공백 순서로 생각하면 읽기 쉽습니다. Boolean 변환이나 String 변환으로 모든 입력을 문자열로 바꾸지 않습니다. 숫자 제목을 문자열로 바꾸면 계약 위반이 숨겨집니다.
문자 수와 요청 바이트 수는 다릅니다
이 교재의 글자 한도는 유니코드 코드 포인트 개수입니다. JavaScript의 length는 UTF-16 코드 단위를 세므로 이모지 일부는 2가 됩니다. 배열 펼치기 [...text].length로 코드 포인트를 셉니다. 화면에서 한 글자로 보이는 결합 문자 묶음과도 다를 수 있습니다. 사용자에게는 코드 포인트 기준임을 알리고, 실제 제품에서 보이는 글자 수가 필요하면 별도 분절 정책을 정합니다. 검사 함수만 바꿔 같은 숫자의 의미를 몰래 바꾸지 않습니다.
HTTP 어댑터는 수신 청크의 바이트를 합쳐 4096바이트를 넘으면 413을 반환합니다. 이 한도는 JSON 파싱 전에 적용합니다. 문자열 검사에서는 2048 코드 포인트인 한글 본문도 JSON 바이트 제한을 넘을 수 있습니다. 그러므로 문자 한도 통과가 HTTP 수락을 보장하지 않습니다. JSON 키·따옴표·공백도 바이트에 포함됩니다. 두 한도를 각자의 위치에서 검사하고 오류 이름도 body_too_large와 invalid_document로 구분합니다.
검증과 정규화 순서를 명시합니다
title은 원문의 크기를 먼저 확인한 후 앞뒤 공백을 제거해 저장합니다. 81개의 공백 뒤에 한 글자가 붙은 제목은 trim하면 짧더라도 원문 한도를 넘으므로 거절합니다. content는 공백과 줄바꿈도 사용자의 자료이므로 그대로 보존합니다. title과 content에 일괄 trim을 적용하면 메모의 들여쓰기나 빈 줄이 바뀝니다. 작은따옴표와 태그 모양 문자는 이 계약에서 금지하지 않습니다. 뒤 레슨의 질의 바인딩과 출력 처리가 맡을 부분입니다.
형식이 올바르면 서버는 title·content를 명시적으로 골라 저장하고 owner는 인증된 세션 사용자 ID에서 얻습니다. 본문 전체를 저장 객체에 펼치는 방식은 나중에 필드가 추가될 때 권한 속성이 섞일 위험이 있습니다. 이번 엄격한 키 검사는 API 계약의 한 선택입니다. 다른 제품이 확장 필드를 무시한다면 그 정책과 테스트를 같이 정해야 합니다. 알 수 없는 필드를 거절하는 선택이 모든 API의 공통 정답은 아닙니다.
실행과 경계 사례를 연결합니다
따라하기에서 자료 검증 함수의 반환값과 코드 포인트 수를 따로 관찰합니다. 브라우저 과제는 정상 문서에 ACCEPT, 계약 위반에 REJECT 한 줄을 출력합니다. 입력 JSON 문법 자체는 유효하다고 가정하므로 문자열을 파싱하는 예외 처리보다 값 판단에 집중합니다. 실제 HTTP 앱에서는 깨진 JSON이 400 invalid_json이며 올바른 JSON이지만 값이 틀리면 400 invalid_document입니다. 상태가 같아도 검사 단계는 다릅니다.
테스트는 제목 80과 81, 본문 2048과 2049, 공백 제목, 빈 본문, 숫자 제목, 배열과 null, 추가 owner, 이모지 제목을 비교합니다. 거절 후에는 새 ID가 소비되거나 기존 자료가 바뀌지 않았는지도 확인합니다. 정상 요청을 전부 거절하는 구현도 보안 테스트 일부는 통과할 수 있으므로 정상 생성과 수정 성공을 나란히 둡니다. 합격 숫자를 늘리기 위해 기대값을 바꾸지 않고 계약과 함수가 다른 지점을 고칩니다.
오류를 읽고 판단을 보고합니다
TypeError에서 trim이 함수가 아니라고 나오면 문자열 검사보다 앞서 trim을 호출했는지 확인합니다. Cannot read properties of null은 최상위 null을 빠뜨린 단서입니다. Unexpected token은 JSON 파싱 단계의 문제이므로 길이 검사를 바꾸어 해결할 수 없습니다. AssertionError에 201과 400이 함께 보이면 어떤 입력이 허용되었는지 테스트 이름과 입력을 읽습니다. 코드 포인트 기준과 바이트 기준을 섞었는지 확인한 뒤 해당 단계만 수정합니다.
보고서에는 합성 입력의 유형·경계값, 수정 전 상태, 수정 후 상태, 정상 대조를 적습니다. 자료 본문 전체를 증거 로그에 복사할 필요는 없습니다. 제목 길이와 상태 코드만으로 입증할 수 있는 사례는 그 정보만 남깁니다. 이 레슨 완료 기준은 숫자를 외우는 것이 아니라 새 필드가 추가되었을 때 유형·허용 키·크기·정규화 순서·거절 후 상태를 스스로 정하고 테스트할 수 있는 것입니다.
따라하기
유형과 공백을 먼저 확인합니다
숫자 제목과 공백 제목을 거절하고 빈 본문을 허용하는지 확인합니다. 아래 코드는 Node.js로 실행하거나 브라우저 JavaScript 실행기에 입력합니다.
function valid(b) {
return !!b && typeof b==='object' && !Array.isArray(b)
&& Object.keys(b).every(k=>['title','content'].includes(k))
&& typeof b.title==='string' && !!b.title.trim() && [...b.title].length<=80
&& typeof b.content==='string' && [...b.content].length<=2048;
}
for(const v of [{title:'메모',content:''},{title:1,content:''},{title:' ',content:''},null,[]])console.log(valid(v)?'ACCEPT':'REJECT');실행 결과
ACCEPT REJECT REJECT REJECT REJECT
제목의 한도 양쪽을 비교합니다
이모지의 코드 포인트 수와 UTF-16 길이를 함께 출력합니다. 허용 상한과 상한을 한 개 넘는 값을 나란히 확인합니다.
function valid(b) {
return !!b && typeof b==='object' && !Array.isArray(b)
&& Object.keys(b).every(k=>['title','content'].includes(k))
&& typeof b.title==='string' && !!b.title.trim() && [...b.title].length<=80
&& typeof b.content==='string' && [...b.content].length<=2048;
}
for(const n of [80,81]){const title='😀'.repeat(n);console.log(n,title.length,[...title].length,valid({title,content:''}));}실행 결과
80 160 80 true 81 162 81 false
본문 한도와 요청 크기를 분리합니다
같은 2048 코드 포인트도 문자에 따라 JSON 바이트 수가 다릅니다. 마지막 행은 HTTP 전체 크기 제한 이전에 값 검사에서 거절되는 경우입니다.
function valid(b) {
return !!b && typeof b==='object' && !Array.isArray(b)
&& Object.keys(b).every(k=>['title','content'].includes(k))
&& typeof b.title==='string' && !!b.title.trim() && [...b.title].length<=80
&& typeof b.content==='string' && [...b.content].length<=2048;
}
for(const content of ['x'.repeat(2048),'가'.repeat(2048),'x'.repeat(2049)]){const b={title:'x',content};console.log([...content].length,Buffer.byteLength(JSON.stringify(b)),valid(b));}실행 결과
2048 2074 true 2048 6170 true 2049 2075 false
확인 문제
실습
유효한 JSON 값 하나를 표준 입력으로 받습니다. title과 content 두 필드만 허용하는 객체인지 검사합니다. 두 값은 문자열이며 title은 원문 80 코드 포인트 이하이고 trim 뒤 비어 있지 않아야 합니다. content는 원문 2048 코드 포인트 이하이며 빈 값은 허용합니다. 조건을 만족하면 ACCEPT, 아니면 REJECT 한 줄만 출력합니다. 길이는 [...문자열].length로 셉니다. 출력에는 디버그 문장을 섞지 않습니다.
모범 답안
const fs=require('node:fs');
const b=JSON.parse(fs.readFileSync(0,'utf8'));
function valid(b) {
return !!b && typeof b==='object' && !Array.isArray(b)
&& Object.keys(b).every(k=>['title','content'].includes(k))
&& typeof b.title==='string' && !!b.title.trim() && [...b.title].length<=80
&& typeof b.content==='string' && [...b.content].length<=2048;
}
console.log(valid(b)?'ACCEPT':'REJECT');
더 읽기
면접 질문
- 취약점 수정 전후의 테스트 내용을 설명합니다.