설명
node-serialize 0.0.4의 unserialize 함수는 일반 데이터만 복원하지 않습니다. 문자열 입력은 먼저 JSON.parse로 변환하지만, 그 뒤 객체와 배열을 재귀적으로 순회하면서 _$$ND_FUNC$$_로 시작하는 문자열 속성을 eval로 평가합니다. 따라서 HTTP 요청에서 온 문자열, 객체 또는 배열이 첫 번째 인자로 전달되면 공격자가 서버 프로세스에서 임의의 JavaScript를 실행할 수 있습니다.
이 패키지의 보안 권고에는 수정 버전이 없습니다. 가장 안전한 해결책은 요청 처리 경로에서 node-serialize와 실행 가능한 객체 복원을 제거하고, 외부 입력을 데이터 전용 형식으로만 처리하는 것입니다.
위험 원인
unserialize함수는 함수 마커를 단순 문자열이 아니라 실행할 코드로 해석합니다.- 입력에
JSON.parse를 먼저 적용해도 안전해지지 않습니다. 파싱된 객체를 다시unserialize에 전달하면 마커 문자열이 그대로 평가됩니다. - 서명은 승인되지 않은 변조를 탐지할 수 있을 뿐입니다. 악의적인 정식 생성자, 유출된 키, 이미 오염된 기존 레코드 또는 디코더 자체의 실행 동작을 안전하게 만들지 않습니다.
- 컨테이너와 최소 권한은 피해 범위를 줄이는 보조 통제입니다. Node.js Permission Model은 악성 코드에 대한 보안 경계가 아닙니다.
잠재적 영향
- 애플리케이션 프로세스 권한으로 임의 코드 또는 명령 실행
- 파일, 환경 변수, 자격 증명 및 내부 서비스 접근
- 데이터 유출·변조와 다른 시스템으로의 확산
- 조작된 레코드가 저장된 경우 반복적인 코드 실행
해결 방법
- 요청 처리 코드와 런타임 의존성에서
node-serialize패키지를 제거하세요. 수정 버전으로 업그레이드하는 선택지는 없습니다. - 외부 데이터는 JSON 같은 데이터 전용 형식으로 파싱하세요. 파싱 전에 엔드포인트에 맞는 바이트 제한을 적용하고, 파싱 후에는 필수 필드, 타입, 길이·범위, 허용되지 않은 추가 필드를 정확히 검증하세요. TypeScript의 인터페이스, 타입 주석, 제네릭 제약 및 타입 단언은 런타임에 제거되므로 검증으로 간주하지 않습니다.
- 검증된 필드만 새 데이터 객체에 복사하세요. 파싱하거나 검증한 결과를
unserialize,eval,Function또는 동적 동작 선택 로직에 다시 전달하지 마세요. - 기존
node-serialize레코드는 네트워크 요청과 분리된 통제된 오프라인 절차에서 데이터 전용 형식으로 마이그레이션하세요. 단기적으로 기존 레코드를 읽어야 한다면 디코딩 전에 저장된 정확한 바이트의 진위와 생성 주체의 권한을 확인하되, 이를 실행 가능한 역직렬화의 영구적인 대안으로 사용하지 마세요. - 마이그레이션은 최소 권한으로 실행하고 완료 후
unserialize호출과 의존성을 제거했는지 검색하고 테스트하세요.
예시
변경 전
javascript
const express = require("express");
let serialize = require("node-serialize");
const app = express();
app.use(express.json({ limit: "64kb", strict: true }));
app.post("/profiles", (req, res) => {
const profile = serialize.unserialize(req.body);
res.json(profile);
});
요청자가 본문에 함수 마커를 넣을 수 있으므로 문자열뿐 아니라 객체 입력도 위험합니다.
변경 후
javascript
const express = require("express");
const app = express();
app.use(express.json({ limit: "64kb", strict: true }));
const allowedProfileKeys = new Set(["displayName", "age"]);
function parseProfile(body) {
if (body === null || typeof body !== "object" || Array.isArray(body)) {
throw new TypeError("profile must be an object");
}
const keys = Object.keys(body);
if (
keys.length !== 2 ||
keys.some((key) => !allowedProfileKeys.has(key)) ||
!Object.hasOwn(body, "displayName") ||
!Object.hasOwn(body, "age")
) {
throw new TypeError("profile has an invalid shape");
}
if (
typeof body.displayName !== "string" ||
body.displayName.length < 1 ||
body.displayName.length > 80 ||
!Number.isInteger(body.age) ||
body.age < 0 ||
body.age > 130
) {
throw new TypeError("profile has invalid values");
}
return {
displayName: body.displayName,
age: body.age,
};
}
app.post("/profiles", (req, res) => {
try {
const profile = parseProfile(req.body);
res.status(201).json(profile);
} catch {
res.status(400).json({ error: "invalid profile" });
}
});
이 예시는 요청 크기를 제한하고, 정확한 데이터 스키마와 도메인 범위를 검사하며, 허용된 값만 새 객체로 만듭니다. 64kb와 필드 제약은 예시이므로 실제 엔드포인트의 데이터 계약에 맞게 더 좁게 조정해야 합니다.
참조
node-serialize보안 경고node-serializeunserialize구현- GitHub Advisory GHSA-q4v7-4rhw-9hqm / CVE-2017-5941
- CWE-502: Deserialization of Untrusted Data
- OWASP Top 10:2025 A08 - Software or Data Integrity Failures
- OWASP Top 10:2021 A08 - Software and Data Integrity Failures
- Express
express.json - Node.js Permission Model