Hacking Mongoose: How I Built a Global Plugin to Stop Data Leaks 🛡️
Source: Dev.to
The Architecture Problem
In a typical Express/Mongoose app, security is often handled in the Controller layer:
// Controller
const user = await User.findById(req.params.id);
const safeUser = omit(user.toObject(), ['password', 'ssn']); // Manual filtering
res.json(safeUser);
This is prone to human error. If you access the database from a background job, a script, or a different controller, you have to remember to apply the same filter.
Security should be defined where the data is defined: in the Schema.
Enter FieldShield
FieldShield is a native Mongoose plugin that allows you to define per‑field access roles directly in your schema definition.
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: [] } } // Hidden from everyone
});
Under the Hood: The pre('find') Hook
The magic happens in how we intercept Mongoose queries. When you install FieldShield, we patch the Mongoose Query prototype to accept a context (roles).
Then, we register a global pre hook that inspects the query before it’s sent to MongoDB.
// Simplified logic from src/query.ts
schema.pre('find', function() {
const roles = this._shieldRoles; // Passed via .role('admin')
const allowedFields = calculateAllowedFields(modelName, roles);
// We force a projection on the query
this.select(allowedFields);
});
This means the database only returns what you are allowed to see. The sensitive data never even enters your Node.js process memory.
The Challenge: Aggregation Pipelines
Simple queries are easy. But what about Model.aggregate()? Aggregations allow arbitrary stages that can reshape documents, making it hard to track fields.
FieldShield solves this by injecting a $project stage dynamically. We analyze your pipeline and insert a protection stage right after the initial $match, ensuring that indexes are used efficiently while data is filtered before it flows through the rest of your pipeline (e.g., into $group or $lookup).
// Input
await User.aggregate([
{ $match: { status: 'active' } },
// ... more stages
]).role('public');
// Actual Pipeline Executed
[
{ $match: { status: 'active' } },
{ $project: { username: 1, _id: 1 } }, // Injected by FieldShield
// ... more stages
]
New in v2.2: Recursive Shield Inheritance 🔄
One of the tough technical challenges solved in v2.2 was Nested Object and Array Inheritance.
In MongoDB, preferences.theme is a distinct path from preferences. If you hide preferences.notifications, does the user still see the preferences object?
We implemented a recursive parser that synthesizes parent permissions based on their children:
- If any child is visible, the parent is visible.
- If all children are hidden, the parent is hidden.
- The parent inherits the union of all child roles.
This ensures you don’t have to manually duplicate shield configs on parent objects.
Performance Verification 🚀
Because FieldShield uses native MongoDB projections, it’s actually faster than fetching the full document and filtering it in JavaScript.
- Network I/O: Reduced (smaller payloads).
- Memory: Reduced (fewer objects created).
- CPU: Reduced (MongoDB handles the filtering in C++).
We Need You! 🫵
FieldShield is fully open source, and we have big plans for v3.0, including:
- 🛡️ Advanced wildcard policies
- 🔍 GraphQL integration
- ⚡ Caching for policy calculation
We are looking for contributors! Whether you’re a TypeScript wizard, a Mongoose expert, or just want to write better docs, we’d love your help.
Good First Issues
- Add more unit tests for edge cases
- Improve documentation examples
- Create a benchmark suite
Star the repo and check out the issues:
👉