DEV Community

Cover image for Hacking Mongoose: How I Built a Global Plugin to Stop Data Leaks 🛡️
Abdelghani El Mouak
Abdelghani El Mouak

Posted on

Hacking Mongoose: How I Built a Global Plugin to Stop Data Leaks 🛡️

Data leaks are the nightmare of every backend developer. You forget one .select('-password') in a new endpoint, and suddenly your user's sensitive data is exposed.

In this deep dive, I'm going to show you how FieldShield solves this problem by hacking into the Mongoose middleware chain to enforce security at the database abstraction level.

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);
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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 but 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
]
Enter fullscreen mode Exit fullscreen mode

New in v2.2: Recursive Shield Inheritance 🔄

One of the tough technical challenges we just 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 field 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 effectively 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:
👉 github.com/kemora13conf/wecon-mongoose-field-shield

Let's build the standard for Mongoose security together.

Top comments (4)

Collapse
 
dashnet13 profile image
Bahou Abdelhamid

Impressive work, I wish for your project to get the recognition it deserves

Collapse
 
kemora_13conf profile image
Abdelghani El Mouak

Thank you bro.

Collapse
 
issam_nexus_50374aa4a207b profile image
issam nexus

Finally, I found the best and easiest approach to protection fields
thnx bro 🔥

Collapse
 
kemora_13conf profile image
Abdelghani El Mouak

Feel free to contribute