SuperAPI AI Directory

meteor-security

Security & auditing

View repository

Updated 10-15 13:55

About this skill

Meteor security

Meteor's security model is opinionated: the server holds authority, the client cannot be trusted, and the only places that filter data before it reaches users are methods (write paths) and publications (read paths).

Decision flow

  1. Audit every method: does it check() every argument and guard on this.userId or Meteor.userId() when authentication matters?
  2. Audit every publication: does it filter by this.userId (when user-specific) and project columns with fields?
  3. Add audit-argument-checks in dev to catch missing check().
  4. Add browser-policy and configure CSP.
  5. Add DDPRateLimiter rules for sensitive methods (login, password reset, resource creation).
  6. If the app uses OAuth, set oauthSecretKey to encrypt provider secrets at rest.
  7. Replace permissive client-write rules with authenticated methods. Keep protective deny rules until an equivalent method-only policy is active; Accounts can add an owner-only profile allowance even without an app allow rule. See the profile policy below.

Accounts collection and profile policy

In Meteor 3.6's fixed Accounts pairing, custom users collections receive account indexes and, when client mutation methods exist, the default owner-only profile allowance. Configure the collection on both runtimes before installing guards. Keep Meteor.users.deny({ update: () => true }) on the selected collection when all client profile writes must be blocked, until an equivalent policy is proven. defineMutationMethods: false supplies a method-only collection without those validators; the RC still creates account indexes. Earlier packages require checking their setup behavior. See meteor-accounts for collection identity, secondary Accounts instances and package boundaries. Never trust roles stored in a client-editable profile.

Method guard checklist

import { Meteor } from "meteor/meteor";
import { check, Match } from "meteor/check";

Meteor.methods({
  async updateProfile(payload) {
    check(payload, { displayName: String, bio: Match.Optional(String) });
    if (!this.userId) {
      throw new Meteor.Error("not-authorized");
    }
    await Meteor.users.updateAsync(this.userId, { $set: { profile: payload } });
  },
  async updateAddress(payload) {
    check(payload, String);
    if (!Meteor.userId()) {
      throw new Meteor.Error("not-authorized");
    }
    await Meteor.users.updateAsync(Meteor.userId(), { $set: { address: payload } });
  },
});

Reject any method that does not match: check on every argument, userId gate when needed, Meteor.Error(code, reason) for failures, *Async Mongo on the server.

Publication guard checklist

Meteor.publish("items.mine", function () {
  if (!this.userId) return this.ready();
  return Items.find(
    { ownerId: this.userId },
    { fields: { title: 1, qty: 1 }, limit: 200 },
  );
});

Reject any publication that returns an unbounded cursor, omits the field projection, or skips a userId filter on user-specific data.

CSP via browser-policy

meteor add browser-policy
// server top-level or inside Meteor.startup
import { BrowserPolicy } from "meteor/browser-policy-common";
import { Meteor } from "meteor/meteor";

Meteor.startup(async () => {
  await BrowserPolicy.content.disallowInlineScripts();
  BrowserPolicy.content.disallowEval();
  BrowserPolicy.framing.disallow();
});

BrowserPolicy is server-only. Configure it during module initialization or startup so every request receives one deterministic process-wide policy. The current implementation invalidates its cached CSP after a mutation, but do not mutate this global policy per request or per user. See references/browser-policy-csp.md for recipes (Stripe, Google Maps, fonts, inline-style allowance).

DDPRateLimiter for sensitive methods

import { DDPRateLimiter } from "meteor/ddp-rate-limiter";

DDPRateLimiter.addRule(
  {
    type: "method",
    name: "login",
    clientAddress: () => true,
  },
  5,
  60000,                  // 5 attempts per 60s, per IP
);

Only matcher fields contribute to the rate-limit bucket key. Without clientAddress, connectionId, or userId, every matching caller shares one global bucket. Meteor 3.5+ permits async matcher functions for database-backed decisions; keep their queries fast because the connection waits for them. On Meteor 3.0 through 3.4, matchers must stay synchronous. Use a fixed rule, precomputed synchronous state, or upgrade rather than awaiting Mongo there.

The default rule (5 in 10s per matched method per DDP connection for login / signup / password reset) ships with accounts-base. Meteor 3.6-beta.1's accounts-base@3.4.0-beta360.1 also matches requestLoginTokenForUser; accounts-passwordless@3.1.2-beta360.1 validates its complete payload before lookup/creation. The RC pairing is accounts-base@3.4.0-rc360.0 with accounts-passwordless@3.1.2-rc360.0; handle callback rate errors without an immediate retry loop. Inspect both package versions on older apps. Remove the default rule with Accounts.removeDefaultRateLimit() only if you replace it.

For cookie hardening on accounts-base@3.4.0-beta360.1, use meteor-accounts: trusted origins, valid tokens, no-store responses and SameSite=Strict have specific migration checks. Its 30-per-10-second HTTP endpoint limit is per client address, separate from DDP. HttpOnly storage does not stop active same-origin XSS from obtaining the resume token used for DDP authentication.

OAuth secret encryption

Add oauth-encryption and pass a 16-byte base64 key (NOT 32 bytes) to Accounts.config at module top level (not inside Meteor.startup):

meteor node -e "console.log(require('crypto').randomBytes(16).toString('base64'))"
import { Accounts } from "meteor/accounts-base";

Accounts.config({
  oauthSecretKey: Meteor.settings.oauthSecretKey,
});

At startup, accounts-oauth seals an unsealed provider application secret at ServiceConfiguration.configurations.secret. Provider packages also seal supported per-user token fields, such as services.github.accessToken or Twitter's accessTokenSecret. There is no generic Meteor.users.services.<provider>.secret field. Inspect the provider schema before asserting which user credential is encrypted.

audit-argument-checks

meteor add audit-argument-checks

Throws if any method or publication runs without check() covering every argument. Methods that legitimately accept arbitrary input declare this explicitly:

Meteor.methods({
  rawLog(...args) {
    check(args, [Match.Any]);
    // ...
  },
});

Anti-patterns

  • Permissive client-write rules for sensitive data. Replace with guarded methods; do not remove protective denial before proving equivalent policy.
  • Meteor.settings.public.<secret>. The client sees public. Move secrets to the top level of settings.json.
  • Publish the entire Meteor.users collection. Always project (e.g. fields: { username: 1, profile: 1 }) and filter. Publish email only to the owning user or another explicitly authorized audience.
  • Use BrowserPolicy.content.allowOriginForAll for a third-party script. It grants the origin to every current content directive. Allow only the script, frame, connect, image, style, or font directives the integration needs.
  • Methods that accept callback-shaped arguments. Functions cannot travel over DDP.
  • Call Accounts.config({ oauthSecretKey }) inside Meteor.startup. Must be at module top level so it loads before the OAuth packages read it.

See also

  • references/method-and-publish-guards.md
  • references/browser-policy-csp.md
  • references/eval-cases.md
  • Related skills: meteor-methods, meteor-pubsub, meteor-accounts.