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

Published: (December 10, 2025 at 05:47 PM EST)
3 min read
Source: Dev.to

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:
👉

Back to Blog

Related posts

Read more »

New to Dev to community

Hey Everyone, I am new to the dev community, starting out my journey again in coding. I used to code from 2013‑2018. After that I explored new opportunities, st...