설명
Ajv에서 allErrors:true 옵션을 사용하면 검증 실패 시 가능한 모든 에러를 수집합니다. 이 과정에서 매우 많은 에러 객체와 문자열이 생성되고, 스키마의 제약 조건을 끝까지 평가하느라 CPU 사용량이 증가합니다. 공격자는 많은 규칙을 동시에 위반하도록 입력을 조작하거나 거대한/중첩된 JSON을 보내 메모리와 CPU를 과도하게 소모시켜 서비스 거부(DoS)를 유발할 수 있습니다.
잠재적 영향
- 서비스 거부(DoS): 과도한 에러 수집과 연산으로 CPU/메모리가 고갈되어 응답 지연 또는 프로세스 다운 발생
- 메모리 고갈: 수천 개의 에러 객체/메시지 생성으로 힙 메모리 급증, OOM 및 재시작 유발
- 성능 저하: 검증이 끝까지 진행되어 요청 처리 시간이 크게 늘어나 전체 서비스 처리량 감소
- 로그/모니터링 비용 증가: 대량의 에러 메시지 출력으로 로그가 폭증하고 운영 비용과 가시성 저하
해결 방법
- 운영(Production)에서는 Ajv 인스턴스 생성 시 allErrors:true를 사용하지 말고 기본값(false, fail-fast)을 유지하세요
- 디버깅이 필요한 경우에만 개발 환경에서 환경 변수로 제어하세요 (예: DEBUG_VALIDATE=1 일 때만 allErrors:true)
- 요청 본문 크기와 연결 시간을 제한하세요. 요청 타임아웃만으로 동기 검증의 CPU 실행 시간을 중단할 수는 없습니다.
- Rate limiting과 Circuit breaker를 적용해 폭주 트래픽을 완화하세요
- 스키마는 신뢰 가능한 소스만 사용하고, 불필요하게 많은 규칙(oneOf/anyOf 등 과도한 조합)을 피하세요
- 입력 구조에 상한선(maxItems, maxProperties 등)을 정의해 검증 복잡도를 낮추세요
예시
변경 전
javascript
import express from "express";
import Ajv from "ajv";
const app = express();
app.use(express.json({ limit: "1mb" }));
// BAD: 운영 환경에서 allErrors:true 사용 (모든 에러를 수집)
const ajv = new Ajv({ allErrors: true });
const userSchema = {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["name", "tags"],
};
const validateUser = ajv.compile(userSchema);
app.post("/users", (req, res) => {
const valid = validateUser(req.body);
if (!valid) {
// 많은 에러를 그대로 생성/반환 -> CPU/메모리 소모
return res.status(400).json({ errors: validateUser.errors });
}
res.send("ok");
});
app.listen(3000);
변경 후
javascript
import express from "express";
import Ajv from "ajv";
const app = express();
// 요청 크기 제한과 타임아웃 설정
app.use(express.json({ limit: "512kb" }));
app.use((req, res, next) => {
req.setTimeout(5000);
next();
});
// GOOD: 기본값(fail-fast). 필요 시 개발 환경에서만 allErrors를 켜기
const enableAllErrors = process.env.NODE_ENV === "development" && process.env.DEBUG_VALIDATE === "1";
const ajv = new Ajv({ allErrors: enableAllErrors });
const userSchema = {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
tags: {
type: "array",
maxItems: 50,
items: { type: "string", maxLength: 64 },
},
},
required: ["name", "tags"],
};
const validateUser = ajv.compile(userSchema);
app.post("/users", (req, res) => {
const valid = validateUser(req.body);
if (!valid) {
// 요약된 메시지로 반환 (대량의 에러 객체 노출 방지)
return res.status(400).json({ message: "Invalid payload" });
}
res.send("ok");
});
app.listen(3000);
설명:
- 변경 전: allErrors:true는 검증이 실패하더라도 모든 규칙을 끝까지 검사하고 모든 에러를 수집합니다. 공격자가 많은 제약을 위반하도록 입력을 만들면 에러 객체가 대량 생성되어 CPU/메모리를 고갈시키고, DoS로 이어질 수 있습니다.
- 변경 후: 개발 환경에서 디버그 플래그를 켠 경우에만
allErrors를 활성화합니다. 그 외에는 추가 오류 수집을 줄이는 기본값을 사용하고, 본문 크기와maxItems·maxLength로 작업량을 제한합니다.req.setTimeout은 연결 타임아웃이며 동기 검증의 실행 시간을 제한하지 않습니다.