Skip to main content

meteor-security

Use when auditing or hardening a Meteor 3 application. Triggers on missing check() on method arguments, missing this.userId guards on publications, browser-policy CSP, DDPRateLimiter rules, oauth-encryption via Accounts.config oauthSecretKey, audit-argument-checks, allow/deny legacy patterns, BrowserPolicy.content.disallowInlineScripts, BrowserPolicy.framing.disallow. Use this skill when the user asks about hardening, asks about a security review, or asks about CSP for a third-party script (Stripe, Google Maps, fonts).

Source facts

Repository
meteor/agent-skills
Last source activity
October 7, 2026 at 18:25
Detected SKILL.md language
English
Stars
10
Forks
1

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
meteor-security
description
Use when auditing or hardening a Meteor 3 application. Triggers on missing check() on method arguments, missing this.userId guards on publications, browser-policy CSP, DDPRateLimiter rules, oauth-encryption via Accounts.config oauthSecretKey, audit-argument-checks, allow/deny legacy patterns, BrowserPolicy.content.disallowInlineScripts, BrowserPolicy.framing.disallow. Use this skill when the user asks about hardening, asks about a security review, or asks about CSP for a third-party script (Stripe, Google Maps, fonts).
metadata
{"author":"meteor","kind":"knowledge","meteor":">=3.0","area":"security","tagline":"Audit and harden Meteor 3 apps (`check()` coverage, `this.userId` guards, browser-policy CSP, rate limits, oauth-encryption).","bundle":["essentials","fullstack"],"docs_synced_at":"2026-10-07"}
license
MIT
# 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 ```javascript 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 ```javascript 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` ```bash meteor add browser-policy ``` ```javascript // 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 ```javascript 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`): ```bash meteor node -e "console.log(require('crypto').randomBytes(16).toString('base64'))" ``` ```javascript 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` ```bash 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: ```javascript 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`.
View on GitHub