Mongoose 해킹: 데이터 유출을 막기 위해 글로벌 플러그인을 만든 방법 🛡️
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.theme은 preferences와 별개의 경로입니다. preferences.notifications을 숨기면 사용자는 여전히 preferences 객체를 볼 수 있을까요?
우리는 자식 필드들을 기반으로 부모 권한을 합성하는 재귀 파서를 구현했습니다:
- 하나라도 자식이 보이면, 부모도 보입니다.
- 모두 숨겨지면, 부모도 숨깁니다.
- 부모는 모든 자식 역할의 합집합을 상속합니다.
이를 통해 부모 객체에 Shield 설정을 일일이 복제할 필요가 없어졌습니다.
성능 검증 🚀
FieldShield는 네이티브 MongoDB 투영을 사용하기 때문에 전체 문서를 가져와 JavaScript에서 필터링하는 방식보다 실제로 더 빠릅니다.
- 네트워크 I/O: 감소 (페이로드가 작아짐).
- 메모리: 감소 (생성되는 객체 수 감소).
- CPU: 감소 (필터링을 MongoDB가 C++로 처리).
여러분의 도움이 필요합니다! 🫵
FieldShield는 완전 오픈 소스이며, v3.0을 위한 큰 계획이 있습니다. 포함될 내용:
- 🛡️ 고급 와일드카드 정책
- 🔍 GraphQL 통합
- ⚡ 정책 계산을 위한 캐싱
우리는 기여자를 찾고 있습니다! TypeScript 마법사이든, Mongoose 전문가이든, 혹은 더 나은 문서를 작성하고 싶든, 여러분의 도움이 필요합니다.
첫 번째 이슈 (Good First Issues)
- 에지 케이스에 대한 단위 테스트 추가
- 문서 예시 개선
- 벤치마크 스위트 만들기
레포를 스타하고 이슈를 확인해 주세요:
👉