Ajv allErrors:true로 인한 리소스 고갈(DoS)

Uncontrolled Resource Consumption (Resource Exhaustion)

설명

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은 연결 타임아웃이며 동기 검증의 실행 시간을 제한하지 않습니다.

참조