Mongoose 해킹: 데이터 유출을 막기 위해 글로벌 플러그인을 만든 방법 🛡️

발행: (2025년 12월 11일 오전 07:47 GMT+9)
6 min read
원문: Dev.to

Source: Dev.to

아키텍처 문제

일반적인 Express/Mongoose 앱에서는 보안이 Controller 레이어에서 처리되는 경우가 많습니다:

// Controller
const user = await User.findById(req.params.id);
const safeUser = omit(user.toObject(), ['password', 'ssn']); // 수동 필터링
res.json(safeUser);

이는 사람의 실수에 취약합니다. 백그라운드 작업, 스크립트, 혹은 다른 컨트롤러에서 데이터베이스에 접근할 때도 같은 필터를 적용해야 한다는 점을 기억해야 합니다.

보안은 데이터가 정의되는 곳, 즉 스키마에서 정의되어야 합니다.

FieldShield 소개

FieldShield는 스키마 정의에 직접 필드별 접근 역할을 지정할 수 있게 해주는 네이티브 Mongoose 플러그인입니다.

const UserSchema = new Schema({
  username: { type: String, shield: { roles: ['public'] } },
  email:    { type: String, shield: { roles: ['owner', 'admin'] } },
  apiKey:   { type: String, shield: { roles: ['admin'] } },
  password: { type: String, shield: { roles: [] } } // 모두에게 숨김
});

내부 동작: pre('find')

마법은 Mongoose 쿼리를 가로채는 방식에서 일어납니다. FieldShield를 설치하면 Mongoose Query 프로토타입을 패치해 컨텍스트(역할)를 받을 수 있게 합니다.

그 다음 전역 pre 훅을 등록해 쿼리가 MongoDB에 전송되기 전에 이를 검사합니다.

// src/query.ts에서 간소화된 로직
schema.pre('find', function() {
  const roles = this._shieldRoles; // .role('admin')을 통해 전달됨
  const allowedFields = calculateAllowedFields(modelName, roles);

  // 쿼리에 투영을 강제 적용
  this.select(allowedFields);
});

즉, 데이터베이스는 당신이 볼 수 있는 필드만 반환합니다. 민감한 데이터는 Node.js 프로세스 메모리에도 들어오지 않습니다.

도전 과제: Aggregation 파이프라인

단순 쿼리는 쉽지만 Model.aggregate()는 어떨까요? Aggregation은 문서를 자유롭게 재구성할 수 있는 단계들을 포함하므로 필드를 추적하기 어렵습니다.

FieldShield는 $project 단계를 동적으로 삽입함으로써 이를 해결합니다. 파이프라인을 분석하고 초기 $match 바로 뒤에 보호 단계를 삽입해 인덱스를 효율적으로 사용하면서 데이터가 파이프라인의 나머지 단계(예: $group, $lookup)로 흐르기 전에 필터링됩니다.

// 입력
await User.aggregate([
  { $match: { status: 'active' } },
  // ... 더 많은 단계
]).role('public');

// 실제 실행된 파이프라인
[
  { $match: { status: 'active' } },
  { $project: { username: 1, _id: 1 } }, // FieldShield가 삽입
  // ... 더 많은 단계
]

v2.2의 새로운 기능: 재귀적 Shield 상속 🔄

v2.2에서 해결한 까다로운 기술 과제 중 하나는 중첩 객체 및 배열 상속이었습니다.

MongoDB에서 preferences.themepreferences와 별개의 경로입니다. preferences.notifications을 숨기면 사용자는 여전히 preferences 객체를 볼 수 있을까요?

우리는 자식 필드들을 기반으로 부모 권한을 합성하는 재귀 파서를 구현했습니다:

  • 하나라도 자식이 보이면, 부모도 보입니다.
  • 모두 숨겨지면, 부모도 숨깁니다.
  • 부모는 모든 자식 역할의 합집합을 상속합니다.

이를 통해 부모 객체에 Shield 설정을 일일이 복제할 필요가 없어졌습니다.

성능 검증 🚀

FieldShield는 네이티브 MongoDB 투영을 사용하기 때문에 전체 문서를 가져와 JavaScript에서 필터링하는 방식보다 실제로 더 빠릅니다.

  • 네트워크 I/O: 감소 (페이로드가 작아짐).
  • 메모리: 감소 (생성되는 객체 수 감소).
  • CPU: 감소 (필터링을 MongoDB가 C++로 처리).

여러분의 도움이 필요합니다! 🫵

FieldShield는 완전 오픈 소스이며, v3.0을 위한 큰 계획이 있습니다. 포함될 내용:

  • 🛡️ 고급 와일드카드 정책
  • 🔍 GraphQL 통합
  • ⚡ 정책 계산을 위한 캐싱

우리는 기여자를 찾고 있습니다! TypeScript 마법사이든, Mongoose 전문가이든, 혹은 더 나은 문서를 작성하고 싶든, 여러분의 도움이 필요합니다.

첫 번째 이슈 (Good First Issues)

  • 에지 케이스에 대한 단위 테스트 추가
  • 문서 예시 개선
  • 벤치마크 스위트 만들기

레포를 스타하고 이슈를 확인해 주세요:
👉

Back to Blog

관련 글

더 보기 »

Dev 커뮤니티 신규 회원

여러분 안녕하세요, 저는 dev 커뮤니티에 새로 온 사람이고 코딩 여정을 다시 시작하고 있습니다. 저는 2013년부터 2018년까지 코딩을 했었습니다. 그 이후에 새로운 기회를 탐색했고, st...