Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/meteor/meteor/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Security in Meteor follows a simple principle: trust the server, validate everything from the client. Understanding the attack surface and implementing proper security measures is critical for any production application.

Security Domains

In a Meteor application, there are two security domains:
  1. Server: Trusted environment where code runs securely
  2. Client: Untrusted environment that can be manipulated
Never trust data coming from the client. Always validate and authorize on the server.

Attack Surface

Secure these three main entry points:

1. Methods

Any data through Method arguments needs validation:
Meteor.methods({
  'posts.create'(title, content) {
    // ✅ Validate all arguments
    check(title, String);
    check(content, String);
    
    // ✅ Check authorization
    if (!this.userId) {
      throw new Meteor.Error('not-authorized');
    }
    
    // ✅ Additional business logic validation
    if (title.length < 3 || title.length > 100) {
      throw new Meteor.Error('invalid-title');
    }
    
    Posts.insert({ title, content, userId: this.userId });
  }
});

2. Publications

Publications must not return unauthorized data:
Meteor.publish('posts.private', function() {
  // ✅ Verify user is logged in
  if (!this.userId) {
    return this.ready();
  }
  
  // ✅ Only return user's own posts
  return Posts.find(
    { userId: this.userId },
    { 
      fields: { 
        title: 1, 
        content: 1,
        // Don't publish sensitive fields
        // apiKey: 0,
        // privateData: 0
      } 
    }
  );
});

3. Served Files

Ensure no secrets in client-accessible code:
// ❌ Never do this - API keys exposed to client
const API_KEY = 'secret-key-12345';

// ✅ Use environment variables on server only
if (Meteor.isServer) {
  const API_KEY = process.env.API_KEY;
}

Remove Insecure Packages

Remove insecure and autopublish packages immediately in any production app.
# Remove insecure packages
meteor remove insecure
meteor remove autopublish

Disable Allow/Deny

Avoid client-side database operations:
// ❌ Never use allow/deny - too complex to secure
Posts.allow({
  insert(userId, doc) {
    return userId && doc.userId === userId;
  },
  update(userId, doc) {
    return userId && doc.userId === userId;
  }
});

// ✅ Instead, deny all client operations
Posts.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; }
});

// ✅ Use Methods for all data modifications
Meteor.methods({
  'posts.insert'(data) {
    // Server-side validation and insertion
  }
});

Validating Method Arguments

Using check()

import { check, Match } from 'meteor/check';

Meteor.methods({
  'posts.update'(postId, updates) {
    // Validate argument types
    check(postId, String);
    check(updates, {
      title: Match.Optional(String),
      content: Match.Optional(String),
      published: Match.Optional(Boolean)
    });
    
    // Validation passed, proceed with logic
  }
});

Using SimpleSchema

import SimpleSchema from 'simpl-schema';

Meteor.methods({
  'posts.insert'(data) {
    // Comprehensive validation
    new SimpleSchema({
      title: {
        type: String,
        min: 3,
        max: 100
      },
      content: {
        type: String,
        min: 10,
        max: 10000
      },
      tags: {
        type: Array,
        optional: true,
        maxCount: 5
      },
      'tags.$': {
        type: String
      },
      publishedAt: {
        type: Date,
        optional: true
      }
    }).validate(data);
    
    // Data is validated, proceed
  }
});

Never Trust this.userId from Client

// ❌ BAD: Client can pass any userId
Meteor.methods({
  'users.setName'({ userId, name }) {
    Meteor.users.update(userId, { $set: { name } });
  }
});

// ✅ GOOD: Use this.userId from DDP
Meteor.methods({
  'users.setName'({ name }) {
    if (!this.userId) {
      throw new Meteor.Error('not-authorized');
    }
    
    Meteor.users.update(this.userId, { $set: { name } });
  }
});

Authorization Patterns

Check Ownership

Meteor.methods({
  'posts.remove'(postId) {
    check(postId, String);
    
    if (!this.userId) {
      throw new Meteor.Error('not-authorized');
    }
    
    const post = Posts.findOne(postId);
    
    if (!post) {
      throw new Meteor.Error('not-found');
    }
    
    // Check if user owns the post
    if (post.userId !== this.userId) {
      throw new Meteor.Error('not-authorized');
    }
    
    Posts.remove(postId);
  }
});

Role-Based Access Control

meteor add alanning:roles
import { Roles } from 'meteor/alanning:roles';

Meteor.methods({
  'users.delete'(userId) {
    check(userId, String);
    
    // Check if current user is admin
    if (!Roles.userIsInRole(this.userId, 'admin')) {
      throw new Meteor.Error('not-authorized');
    }
    
    // Admin can delete users
    Meteor.users.remove(userId);
  }
});

// Assign roles
Roles.addUsersToRoles(userId, ['admin'], Roles.GLOBAL_GROUP);

// Check roles
if (Roles.userIsInRole(userId, 'admin')) {
  // User is admin
}

Rate Limiting

Protect against brute force and spam:
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
import { Accounts } from 'meteor/accounts-base';

// Rate limit login attempts
Accounts.addDefaultRateLimit();

// Custom rate limiting for Methods
const SENSITIVE_METHODS = [
  'posts.create',
  'posts.update',
  'posts.remove',
  'comments.create'
];

if (Meteor.isServer) {
  DDPRateLimiter.addRule({
    type: 'method',
    name(name) {
      return SENSITIVE_METHODS.includes(name);
    },
    connectionId() { 
      return true; 
    }
  }, 5, 1000); // 5 calls per second per connection
  
  // Rate limit by user ID
  DDPRateLimiter.addRule({
    type: 'method',
    name: 'posts.create',
    userId(userId) { 
      return !!userId; 
    }
  }, 10, 60000); // 10 posts per minute per user
}

Securing Publications

Field Filtering

// ✅ Only publish safe fields
Meteor.publish('users.profile', function(userId) {
  check(userId, String);
  
  return Meteor.users.find(
    { _id: userId },
    {
      fields: {
        username: 1,
        'profile.name': 1,
        'profile.avatar': 1,
        // NEVER publish:
        // services: 0,        // OAuth tokens
        // 'services.password': 0,  // Password hashes
        // emails: 0,         // Unless necessary
        // apiKeys: 0         // API credentials
      }
    }
  );
});

Validate Publication Arguments

import { check } from 'meteor/check';

Meteor.publish('posts.byCategory', function(category, limit) {
  // Validate arguments
  check(category, String);
  check(limit, Number);
  
  // Enforce maximum limit
  limit = Math.min(limit, 100);
  
  return Posts.find(
    { category },
    { limit, sort: { createdAt: -1 } }
  );
});

Check User Permissions

Meteor.publish('admin.statistics', function() {
  if (!Roles.userIsInRole(this.userId, 'admin')) {
    // Return nothing for non-admins
    return this.ready();
  }
  
  return Statistics.find();
});

Secrets Management

Environment Variables

// settings.json (excluded from version control)
{
  "private": {
    "MAIL_URL": "smtp://username:password@smtp.example.com:587",
    "AWS_ACCESS_KEY": "AKIAIOSFODNN7EXAMPLE",
    "AWS_SECRET_KEY": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
  },
  "public": {
    "APP_NAME": "My App"
  }
}
# Run with settings
meteor --settings settings.json
// Access in code
if (Meteor.isServer) {
  // Server-only secrets
  const awsKey = Meteor.settings.private.AWS_ACCESS_KEY;
  
  // Or use process.env
  const mailUrl = process.env.MAIL_URL;
}

if (Meteor.isClient) {
  // Public settings available on client
  const appName = Meteor.settings.public.APP_NAME;
}

Never Commit Secrets

# .gitignore
settings.json
settings-*.json
.env
*.env

Input Sanitization

Prevent XSS (Cross-Site Scripting)

import { check } from 'meteor/check';

Meteor.methods({
  'comments.create'(postId, text) {
    check(postId, String);
    check(text, String);
    
    // Meteor's templating automatically escapes HTML
    // But be careful with raw HTML or dangerouslySetInnerHTML
    
    Comments.insert({
      postId,
      text, // Will be escaped in templates
      userId: this.userId,
      createdAt: new Date()
    });
  }
});
<!-- ✅ Safe: Blaze automatically escapes -->
<div>{{comment.text}}</div>

<!-- ❌ Dangerous: Raw HTML -->
<div>{{{comment.text}}}</div>

<!-- ✅ Use sanitization library if needed -->
<div>{{sanitizeHtml comment.text}}</div>

Prevent NoSQL Injection

// ❌ BAD: Allows MongoDB operators
Meteor.methods({
  'posts.findByTitle'(title) {
    return Posts.find({ title }); // Client could pass { $ne: null }
  }
});

// ✅ GOOD: Validate type
Meteor.methods({
  'posts.findByTitle'(title) {
    check(title, String); // Ensures it's a string
    return Posts.find({ title });
  }
});

HTTPS/SSL

Always use HTTPS in production to encrypt data in transit.
// Force SSL in production
if (Meteor.isProduction) {
  // Redirect HTTP to HTTPS
  WebApp.connectHandlers.use(function(req, res, next) {
    if (req.headers['x-forwarded-proto'] !== 'https') {
      res.writeHead(301, {
        Location: 'https://' + req.headers.host + req.url
      });
      res.end();
      return;
    }
    next();
  });
}

Content Security Policy

import { BrowserPolicy } from 'meteor/browser-policy-common';

if (Meteor.isServer) {
  // Disable inline scripts
  BrowserPolicy.content.disallowInlineScripts();
  
  // Allow specific domains
  BrowserPolicy.content.allowScriptOrigin('https://cdn.example.com');
  BrowserPolicy.content.allowImageOrigin('https://images.example.com');
  
  // Allow self
  BrowserPolicy.content.allowScriptOrigin("'self'");
  BrowserPolicy.content.allowStyleOrigin("'self'");
}

Security Checklist

1

Remove insecure packages

meteor remove insecure autopublish
2

Validate all Method arguments

Use check() or SimpleSchema for every Method
3

Disable client-side database operations

Use deny() rules on all collections
4

Filter publication fields

Never publish sensitive user data or credentials
5

Implement rate limiting

Protect against brute force and spam
6

Use HTTPS in production

Encrypt all data in transit
7

Manage secrets properly

Use environment variables, never commit secrets
8

Implement authorization

Check ownership and roles before operations
9

Enable audit logging

Log security-relevant actions
10

Keep dependencies updated

Regularly update Meteor and npm packages