외부 입력을 객체로 파싱하는 일은 값을 읽는 단계일 뿐, 내부 명령이나 권한을 얻었다는 뜻이 아닙니다. 안전한 경계는 제한된 원문을 하나의 해석으로 검증하고 허용 필드만 새 객체로 옮긴 뒤, 대상과 권한을 현재 서버 문맥에서 다시 결정하는 순서로 만들어집니다.
JSON 형식과 후속 처리 안전성의 분리
설정 API가 JSON 객체를 받은 뒤 내부 기본 설정에 재귀적으로 병합한다고 합시다. JSON 파싱 자체가 코드를 실행하지 않아도, 병합기가 외부 키를 따라 내부 prototype이나 보안 설정을 수정하면 그 다음 요청의 권한 판단이 바뀔 수 있습니다. 안전성은 파일 확장자보다 데이터가 어떤 동작을 선택하게 되는지에 달려 있습니다.
반면 일반 객체 직렬화 형식은 입력에 포함된 클래스 이름이나 타입 정보에 따라 객체를 만들거나 생성자·복원 후크를 호출할 수 있습니다. 이 경우 복원은 바이트를 단순한 값으로 읽는 데서 끝나지 않고 객체 생성이나 후크 실행을 포함할 수 있습니다. 따라서 비신뢰 입력이 복원 대상을 선택하게 두면 단순 데이터 읽기보다 실행 표면이 넓어지며, 텍스트인지 바이너리인지보다 어떤 동작을 선택하게 하는지를 기준으로 봐야 합니다.
파싱·구조 검증·도메인 변환의 분리
| 단계 | 검사 | 아직 증명되지 않은 것 |
|---|---|---|
| 전송 제한 | 바이트·기한·압축 후 크기 | 올바른 스키마 |
| 데이터 파싱 | 허용 형식·깊이·중복 키 정책 | 필드의 업무 의미 |
| 스키마 검증 | 필드·타입·범위·길이 | 대상에 대한 사용자 권한 |
| 내부 객체 생성 | 허용 필드만 복사·서버 값 결합 | 실행 시점 최신 상태 |
| 인가·변경 | 실제 대상·현재 정책 확인 | 외부 부수 효과의 원자성 |
예를 들어 파서는 중복 키를 앞값으로 읽는데 뒤의 검증기가 뒷값을 읽으면, 같은 바이트가 파싱 단계와 검증 단계에서 다른 의미가 됩니다. 그래서 중복 키 처리 방식을 하나로 맞추고, 숫자의 범위·정수 정밀도·부동소수 특수값과 문자열 정규화 규칙을 파싱·검증 사이의 같은 계약으로 둡니다. 입력에 서명이 붙어 있어도 신뢰한 출처와 용도, 재생 여부와 버전이 맞는지 확인한 뒤 업무 규칙을 적용해야 합니다.
그림을 글로 읽기
- 비신뢰 원문 바이트
- 제한된 데이터 파싱
- 엄격한 스키마 검증
- 새 내부 객체 생성: 허용 필드 + 서버 인증 문맥
- 비신뢰 원문 바이트 → 제한된 데이터 파싱: 자원·형식 제한
- 제한된 데이터 파싱 → 엄격한 스키마 검증: 필드 의미 확인
- 엄격한 스키마 검증 → 새 내부 객체 생성: 명시적 변환
실패를 진단할 때 raw → parsed → validated → command의 각 표현을 기록하되 비밀 값은 제거합니다. 파싱은 성공했지만 허용되지 않은 필드가 남았거나, 구조 검증은 통과했지만 principal의 대상 권한이 없거나, 명령 생성 뒤 외부 호출이 증폭되는 경우를 서로 다른 실패로 분류해야 보안 성공률을 과대평가하지 않습니다.
Prototype 상속값과 입력의 실제 필드 구분
JavaScript에서 객체 속성을 읽을 때는 객체 자체에 저장된 필드(own property)뿐 아니라 prototype이라는 상속 체인의 값도 보일 수 있습니다. 따라서 input.role이나 for...in만 보고 입력이 실제로 보낸 필드라고 판단하지 말고, Object.hasOwn(input, key)로 own property인지 확인한 뒤 검증된 허용 필드만 새 객체에 복사합니다.
그래도 이 확인은 필드가 입력에 있었다는 것만 말하므로, 그 값이 허용된 타입인지와 해당 작업을 할 권한이 있는지는 별도로 판단해야 합니다.
function toRenameCommand(input, principal) {
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
throw new Error('invalid object');
}
const keys = Object.keys(input);
if (keys.length !== 1 || keys[0] !== 'displayName') {
throw new Error('unexpected fields');
}
if (typeof input.displayName !== 'string' || input.displayName.length > 80) {
throw new Error('invalid name');
}
return { userId: principal.userId, displayName: input.displayName };
}
이는 일반 JSON 파서가 만든 데이터 전용 객체라는 전제의 예제입니다. 임의 객체의 getter·Proxy까지 안전하게 실행한다는 범용 sanitizer가 아닙니다. 보안 역할을 입력에서 복사하지 않고 서버의 principal에서 주체를 가져오는 것이 핵심입니다. 실제 이름 정책은 빈 값·문자 수와 바이트 수·표시 정규화도 별도로 정합니다.
__proto__·constructor·prototype 등 민감 경로를 통한 오염을 고려하되 몇 문자열만 차단한 뒤 무제한 재귀 병합을 안전하다고 선언하지 않습니다. 사전 용도의 Map이나 null-prototype 객체는 상속 충돌을 줄일 수 있지만 값 검증·후속 일반 객체로의 변환·권한 분리를 대신하지 않습니다.
파싱 자원 상한과 입력 계약
작은 압축 입력이 큰 원문이 되거나 깊은 중첩·수많은 항목이 파서와 스키마 검증기의 CPU·메모리를 많이 사용할 수 있습니다. 전송 크기뿐 아니라 해제 후 크기·깊이·항목 수·문자열 길이·총 처리 기한을 제한합니다. 파싱 이후 외부 API를 호출하는 필드가 있다면 항목 수가 호출 수로 증폭되지 않게 상한과 허용 범위를 둡니다.
임의 클래스 복원이 정말 필요한 내부 프로토콜도 출처를 좁히고 타입 허용 목록·버전·서명·재생 방지를 적용합니다. 가능한 외부 API에서는 필요한 데이터를 읽어 명시적으로 내부 객체를 만드는 쪽이 검토 범위를 줄입니다.
입력 처리의 부수 효과와 후속 요청 영향
테스트에서는 알 수 없는 필드·상속된 필드·깊은 구조·중복 키·큰 정수·오래된 스키마를 넣습니다. 잘못된 입력이 거절될 뿐 아니라 내부 기본 설정과 다른 요청의 객체가 바뀌지 않았는지 확인합니다. 테스트용 전역 prototype 변경은 격리 프로세스에서 실행하고 항상 복원해야 합니다.
파싱 성공률을 보안 성공률로 보고하지 않습니다. 거절 뒤 부수 효과가 없고, 정상 입력은 같은 도메인 명령이 되며, 현재 사용자의 대상 권한이 실행점에서 유지되는지가 기준입니다. 위 코드는 학습 예제이며 이 노트의 테스트 목록을 모두 실행한 것은 아닙니다.