Appearance
生产环境应用安全
¥Application Security for Production
阅读本教程后,你将了解:
¥After reading this tutorial, you'll know:
Meteor 应用的安全表面。
¥The security surface area of a Meteor app.
如何保护 Meteor 方法、发布和源代码。
¥How to secure Meteor Methods, publications, and source code.
用于存储开发和生产环境中的密钥。
¥Where to store secret keys in development and production.
如何在审核应用时遵循安全检查清单。
¥How to follow a security checklist when auditing your app.
Galaxy Hosting 中的应用保护机制。
¥How App Protection works in Galaxy Hosting.
简介
¥Introduction
保护 Web 应用的关键在于理解安全域以及这些域之间的攻击面。在 Meteor 应用中,情况就简单多了:
¥Securing a web application is all about understanding security domains and understanding the attack surface between these domains. In a Meteor app, things are pretty simple:
在服务器端运行的代码是可信的。
¥Code that runs on the server can be trusted.
其他所有内容:在客户端运行的代码、通过方法和发布参数发送的数据等都不可信。
¥Everything else: code that runs on the client, data sent through Method and publication arguments, etc, can't be trusted.
实际上,这意味着你应该在这两个域的边界上进行大部分安全验证工作。简而言之:
¥In practice, this means that you should do most of your security and validation on the boundary between these two domains. In simple terms:
验证并检查来自客户端的所有输入。
¥Validate and check all inputs that come from the client.
不要向客户端泄露任何机密信息。
¥Don't leak any secret information to the client.
概念:攻击面
¥Concept: Attack surface
由于 Meteor 应用通常采用将客户端和服务器代码放在一起的编写方式,因此了解客户端运行的代码、服务器运行的代码以及它们的边界至关重要。以下是 Meteor 应用中需要进行安全检查的完整列表:
¥Since Meteor apps are often written in a style that puts client and server code together, it's extra important to be aware what is running on the client, what is running on the server, and what the boundaries are. Here's a complete list of places security checks need to be done in a Meteor app:
方法:通过方法参数传入的任何数据都需要进行验证,并且方法不应返回用户无权访问的数据。
¥Methods: Any data that comes in through Method arguments needs to be validated, and Methods should not return data the user shouldn't have access to.
发布:通过发布参数传入的任何数据都需要进行验证,并且发布不应返回用户无权访问的数据。
¥Publications: Any data that comes in through publication arguments needs to be validated, and publications should not return data the user shouldn't have access to.
已提供的文件:你应该确保提供给客户端的任何源代码或配置文件中都不包含任何敏感数据。
¥Served files: You should make sure none of the source code or configuration files served to the client have secret data.
以下将分别介绍这些要点。
¥Each of these points will have their own section below.
避免使用 allow/deny 规则
¥Avoid allow/deny
在本指南中,我们将明确指出,直接从客户端使用 allow 或 deny 运行 MongoDB 查询并非明智之举。主要原因是很难遵循上述原则。验证所有可能的 MongoDB 操作符极其困难,而且随着 MongoDB 版本的更新,操作符的数量可能会不断增加。
¥In this guide, we're going to take a strong position that using allow or deny to run MongoDB queries directly from the client is not a good idea. The main reason is that it is hard to follow the principles outlined above. It's extremely difficult to validate the complete space of possible MongoDB operators, which could potentially grow over time with new versions of MongoDB.
Discover Meteor 博客上已经有多篇文章讨论了从客户端(特别是 允许与拒绝安全挑战 及其 results)接收 MongoDB 更新操作符的潜在陷阱。
¥There have been several articles about the potential pitfalls of accepting MongoDB update operators from the client, in particular the Allow & Deny Security Challenge and its results, both on the Discover Meteor blog.
鉴于以上几点,我们建议所有 Meteor 应用都应使用方法来接收来自客户端的数据输入,并尽可能严格地限制每个方法接受的参数。
¥Given the points above, we recommend that all Meteor apps should use Methods to accept data input from the client, and restrict the arguments accepted by each Method as tightly as possible.
以下代码片段可添加到你的服务器代码中,用于禁用集合的客户端更新。这将确保你的应用的其他部分不能将 allow 与集合 Lists 一起使用:
¥Here's a code snippet to add to your server code which disables client-side updates on a collection. This will make sure no other part of your app can use allow with the collection Lists:
js
// Deny all client-side updates on the Lists collection
Lists.deny({
insert() { return true; },
update() { return true; },
remove() { return true; },
});方法
¥Methods
方法是 Meteor 服务器接收来自外部的输入和数据的方式,因此它们自然是安全性的重中之重。如果你没有妥善保护你的方法,用户最终可能会以意想不到的方式修改你的数据库。 - 编辑他人的文档、删除数据或破坏数据库架构,导致应用崩溃。
¥Methods are the way your Meteor server accepts inputs and data from the outside world, so it's natural that they are the most important topic for security. If you don't properly secure your Methods, users can end up modifying your database in unexpected ways - editing other people's documents, deleting data, or messing up your database schema causing the app to crash.
验证所有参数
¥Validate all arguments
如果可以假设输入正确,编写简洁的代码就容易得多,因此在运行任何实际业务逻辑之前验证所有方法参数非常重要。你不希望有人传递你未预期的数据类型,从而导致意外行为。
¥It's much easier to write clean code if you can assume your inputs are correct, so it's valuable to validate all Method arguments before running any actual business logic. You don't want someone to pass a data type you aren't expecting and cause unexpected behavior.
请注意,如果你要为方法编写单元测试,则需要测试方法的所有可能输入类型;验证参数可以限制需要进行单元测试的输入范围,从而减少总体代码量。它还有一个额外的优点,那就是具有自文档性;其他人可以阅读代码,了解方法需要哪些类型的参数。
¥Consider that if you are writing unit tests for your Methods, you would need to test all possible kinds of input to the Method; validating the arguments restricts the space of inputs you need to unit test, reducing the amount of code you need to write overall. It also has the extra bonus of being self-documenting; someone else can come along and read the code to find out what kinds of parameters a Method is looking for.
举个例子,以下情况如果不检查参数可能会造成灾难性后果:
¥Just as an example, here's a situation where not checking arguments can be disastrous:
js
Meteor.methods({
removeWidget(id) {
if (!this.userId) {
throw new Meteor.Error('removeWidget.unauthorized');
}
Widgets.remove(id);
}
});如果有人传递一个非 ID 选择器(例如 {}),最终会导致整个集合被删除。
¥If someone comes along and passes a non-ID selector like {}, they will end up deleting the entire collection.
jam:method
为了帮助你编写能够全面验证参数的优秀方法,你可以使用一个用于强制执行参数验证的方法社区包。了解更多关于如何在 jam:method 文档 中使用 Cordova 的信息。本文其余代码示例均假设你正在使用此软件包。如果不是,你仍然可以应用相同的原则,但代码看起来会略有不同。
¥To help you write good Methods that exhaustively validate their arguments, you can use a community package for Methods that enforces argument validation. Read more about how to use it in the documentation for jam:method. The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different.
切勿从客户端传递 userId
¥Never pass userId from the client
每个 Meteor 方法中的 this 上下文都包含一些关于当前连接的有用信息,其中最有用的是 this.userId。此属性由 DDP 登录系统管理,并由框架本身保证其安全性符合广泛使用的最佳实践。
¥The this context inside every Meteor Method has some useful information about the current connection, and the most useful is this.userId. This property is managed by the DDP login system, and is guaranteed by the framework itself to be secure following widely-used best practices.
鉴于当前用户的用户 ID 可通过此上下文获取,因此你绝不应将当前用户的 ID 作为参数传递给方法。这将允许你的应用的任何客户端传递他们想要的任何用户 ID。我们来看一个例子:
¥Given that the user ID of the current user is available through this context, you should never pass the ID of the current user as an argument to a Method. This would allow any client of your app to pass any user ID they want. Let's look at an example:
js
// #1: Bad! The client could pass any user ID and set someone else's name
async run({ userId, newName }) {
await Meteor.users.updateAsync(userId, {
$set: { name: newName }
});
}
// #2: Good, the client can only set the name on the currently logged in user
async run({ newName }) {
await Meteor.users.updateAsync(this.userId, {
$set: { name: newName }
});
}只有在以下情况下才需要将用户 ID 作为参数传递:
¥The only times you should be passing any user ID as an argument are the following:
这是一个只有管理员用户才能访问的方法,管理员可以编辑其他用户。请参阅 用户角色 部分,了解如何定义角色以及如何检查用户是否属于特定角色。
¥This is a Method only accessible by admin users, who are allowed to edit other users. See the section about user roles to learn how to define roles and check that a user is in a certain role.
此方法不会修改其他用户,而是将其用作目标;例如,它可以是发送私信或添加用户为好友的方法。
¥This Method doesn't modify the other user, but uses it as a target; for example, it could be a Method for sending a private message, or adding a user as a friend.
每个操作使用一个方法
¥One Method per action
确保应用安全的最佳方法是了解所有可能来自不受信任来源的输入,并确保正确处理它们。要了解客户端可以接收哪些输入,最简单的方法是尽可能缩小输入范围。这意味着你的所有方法都应该是具体的操作,并且不应该接受大量会显著改变行为的选项。最终目标是你可以查看应用中的每个方法,并验证或测试其安全性。以下是 Todos 示例应用中一个安全的示例方法:
¥The best way to make your app secure is to understand all of the possible inputs that could come from an untrusted source, and make sure that they are all handled correctly. The easiest way to understand what inputs can come from the client is to restrict them to as small of a space as possible. This means your Methods should all be specific actions, and shouldn't take a multitude of options that change the behavior in significant ways. The end goal is that you can look at each Method in your app and validate or test that it is secure. Here's a secure example Method from the Todos example app:
js
export const makePrivate = createMethod({
name: 'lists.makePrivate',
schema: new SimpleSchema({
listId: { type: String }
}),
async run({ listId }) {
if (!this.userId) {
throw new Meteor.Error('lists.makePrivate.notLoggedIn',
'Must be logged in to make private lists.');
}
const list = await Lists.findOneAsync(listId);
if (list.isLastPublicList()) {
throw new Meteor.Error('lists.makePrivate.lastPublicList',
'Cannot make the last public list private.');
}
await Lists.updateAsync(listId, {
$set: { userId: this.userId }
});
Lists.userIdDenormalizer.set(listId, this.userId);
}
});可以看到,这个方法执行的是一个非常具体的操作。 - 它将单个列表设为私有。另一种方法是创建一个名为 setPrivacy 的方法,用于将列表设置为私有或公共,但事实证明,在这个特定的应用中,这两个相关操作的安全考虑因素 - makePrivate 和 makePublic - 截然不同。通过把操作拆分成不同的方法,我们可以让每个操作都更加清晰明了。从上面的方法定义中可以清楚地看出我们接受哪些参数、执行哪些安全检查以及对数据库执行哪些操作。
¥You can see that this Method does a very specific thing - it makes a single list private. An alternative would have been to have a Method called setPrivacy, which could set the list to private or public, but it turns out that in this particular app the security considerations for the two related operations - makePrivate and makePublic - are very different. By splitting our operations into different Methods, we make each one much clearer. It's obvious from the above Method definition which arguments we accept, what security checks we perform, and what operations we do on the database.
但这并不意味着你在编写方法时不能保持灵活性。我们来看一个例子:
¥However, this doesn't mean you can't have any flexibility in your Methods. Let's look at an example:
js
Meteor.users.methods.setUserData = createMethod({
name: 'Meteor.users.methods.setUserData',
schema: new SimpleSchema({
fullName: { type: String, optional: true },
dateOfBirth: { type: Date, optional: true },
}),
async run(fieldsToSet) {
return (await Meteor.users.updateAsync(this.userId, {
$set: fieldsToSet
}));
}
});上述方法非常棒,因为你可以灵活地设置一些可选字段,并仅传递你想要更改的字段。具体来说,此方法之所以可行,是因为设置全名和出生日期的安全考虑因素相同。 - 我们无需针对设置的不同字段执行不同的安全检查。请注意,必须在服务器端生成 MongoDB 上的 $set 查询。 - 我们绝不应该直接从客户端获取 MongoDB 操作符,因为它们难以验证,并且可能会导致意外的副作用。
¥The above Method is great because you can have the flexibility of having some optional fields and only passing the ones you want to change. In particular, what makes it possible for this Method is that the security considerations of setting one's full name and date of birth are the same - we don't have to do different security checks for different fields being set. Note that it's very important that the $set query on MongoDB is generated on the server - we should never take MongoDB operators as-is from the client, since they are hard to validate and could result in unexpected side effects.
重构以重用安全规则
¥Refactoring to reuse security rules
你可能会遇到这样的情况:应用中的许多方法都具有相同的安全检查。可以通过将安全机制提取到单独的模块中、封装方法体或扩展 Mongo.Collection 类来简化此操作,从而在服务器上的 insert、update 和 remove 实现中处理安全问题。但是,通过特定方法实现客户端-服务器通信仍然是一个好主意,而不是从客户端发送任意的 update 运算符,因为恶意客户端无法发送你未测试过的 update 运算符。
¥You might run into a situation where many Methods in your app have the same security checks. This can be simplified by factoring out the security into a separate module, wrapping the Method body, or extending the Mongo.Collection class to do security inside the insert, update, and remove implementations on the server. However, implementing your client-server communication via specific Methods is still a good idea rather than sending arbitrary update operators from the client, since a malicious client can't send an update operator that you didn't test for.
速率限制
¥Rate limiting
与 REST 端点类似,Meteor 方法可以从任何位置调用。 - 恶意程序、浏览器控制台中的脚本等等,很容易在极短时间内触发大量方法调用。这意味着攻击者很容易测试各种不同的输入,找到一个有效的输入。Meteor 内置了密码登录速率限制,以防止密码暴力破解,但你需要自行定义其他方法的速率限制。
¥Like REST endpoints, Meteor Methods can be called from anywhere - a malicious program, script in the browser console, etc. It is easy to fire many Method calls in a very short amount of time. This means it can be easy for an attacker to test lots of different inputs to find one that works. Meteor has built-in rate limiting for password login to stop password brute-forcing, but it's up to you to define rate limits for your other Methods.
在 Todos 示例应用中,我们使用以下代码为所有方法设置基本速率限制:
¥In the Todos example app, we use the following code to set a basic rate limit on all Methods:
js
// Get list of all method names on Lists
const LISTS_METHODS = _.pluck([
insert,
makePublic,
makePrivate,
updateName,
remove,
], 'name');
// Only allow 5 list operations per connection per second
if (Meteor.isServer) {
DDPRateLimiter.addRule({
name(name) {
return _.contains(LISTS_METHODS, name);
},
// Rate limit per connection ID
connectionId() { return true; }
}, 5, 1000);
}这将限制每个方法在每个连接每秒只能调用 5 次。这是一个用户完全感觉不到的速率限制,但它可以防止恶意脚本用请求淹没服务器。你需要根据应用的需求调整限制参数。
¥This will make every Method only callable 5 times per second per connection. This is a rate limit that shouldn't be noticeable by the user at all, but will prevent a malicious script from totally flooding the server with requests. You will need to tune the limit parameters to match your app's needs.
如果你使用的是 jam:method,它自带 rate-limiting。
¥If you're using jam:method, it comes with built-in rate-limiting.
发布
¥Publications
发布是 Meteor 服务器向客户端提供数据的主要方式。对于 Methods 来说,主要关注点是确保用户无法以意想不到的方式修改数据库;而对于 Publishers 来说,主要问题在于过滤返回的数据,以防止恶意用户访问他们不应该看到的数据。
¥Publications are the primary way a Meteor server can make data available to a client. While with Methods the primary concern was making sure users can't modify the database in unexpected ways, with publications the main issue is filtering the data being returned so that a malicious user can't get access to data they aren't supposed to see.
无法在渲染层进行安全设置
¥You can't do security at the rendering layer
在像 Ruby on Rails 这样的服务器端渲染框架中,只需确保返回的 HTML 响应中不显示敏感数据即可。在 Meteor 中,由于渲染是在客户端完成的,因此在 HTML 模板中使用 if 语句并不安全;你需要在数据层面进行安全防护,以确保数据从一开始就不会被发送。
¥In a server-side-rendered framework like Ruby on Rails, it's sufficient to not display sensitive data in the returned HTML response. In Meteor, since the rendering is done on the client, an if statement in your HTML template is not secure; you need to do security at the data level to make sure that data is never sent in the first place.
方法规则仍然适用
¥Rules about Methods still apply
以上关于方法的所有要点也适用于发布:
¥All of the points above about Methods apply to publications as well:
使用
check或 npmsimpl-schema验证所有参数。¥Validate all arguments using
checkor npmsimpl-schema.切勿将当前用户 ID 作为参数传递。
¥Never pass the current user ID as an argument.
不要使用通用参数;请确保你清楚了解你的发布内容从客户端获取的信息。
¥Don't take generic arguments; make sure you know exactly what your publication is getting from the client.
使用速率限制来阻止用户向你发送大量订阅请求。
¥Use rate limiting to stop people from spamming you with subscriptions.
始终限制字段
¥Always restrict fields
Mongo.Collection#find has an option called projection 允许你筛选已获取文档中的字段。在发布时,你应该始终使用此功能,以确保你不会意外发布敏感字段。
¥Mongo.Collection#find has an option called projection which lets you filter the fields on the fetched documents. You should always use this in publications to make sure you don't accidentally publish secret fields.
例如,你可以编写一个发布,然后稍后向已发布的集合添加一个秘密字段。现在,发布会将该密钥发送给客户端。如果你在首次编写每个发布时都对字段进行过滤,那么添加另一个字段不会自动发布。
¥For example, you could write a publication, then later add a secret field to the published collection. Now, the publication would be sending that secret to the client. If you filter the fields on every publication when you first write it, then adding another field won't automatically publish it.
js
// #1: Bad! If we add a secret field to Lists later, the client
// will see it
Meteor.publish('lists.public', function () {
return Lists.find({userId: {$exists: false}});
});
// #2: Good, if we add a secret field to Lists later, the client
// will only publish it if we add it to the list of fields
Meteor.publish('lists.public', function () {
return Lists.find({userId: {$exists: false}}, {
projection: {
name: 1,
incompleteCount: 1,
userId: 1
}
});
});如果你发现自己经常重复使用某些字段,那么提取一个公共字段字典并始终按此进行过滤就很有意义,如下所示:
¥If you find yourself repeating the fields often, it makes sense to factor out a dictionary of public fields that you can always filter by, like so:
js
// In the file where Lists is defined
Lists.publicFields = {
name: 1,
incompleteCount: 1,
userId: 1
};现在,你的代码会变得更简洁一些:
¥Now your code becomes a bit simpler:
js
Meteor.publish('lists.public', function () {
return Lists.find({userId: {$exists: false}}, {
projection: Lists.publicFields
});
});发布和用户 ID
¥Publications and userId
从发布返回的数据通常取决于当前登录用户,以及该用户的某些属性。 - 无论他们是否是管理员,是否拥有某个文档等等。
¥The data returned from publications will often be dependent on the currently logged in user, and perhaps some properties about that user - whether they are an admin, whether they own a certain document, etc.
发布不是响应式的,它们仅在当前登录的 userId 发生更改时才会重新运行,可以通过 this.userId 访问。因此,很容易意外地编写出一个在首次运行时安全,但无法响应应用环境变化的发布。我们来看一个例子:
¥Publications are not reactive, and they only re-run when the currently logged in userId changes, which can be accessed through this.userId. Because of this, it's easy to accidentally write a publication that is secure when it first runs, but doesn't respond to changes in the app environment. Let's look at an example:
js
// #1: Bad! If the owner of the list changes, the old owner will still see it
Meteor.publish('list', async function (listId) {
check(listId, String);
const list = await Lists.findOneAsync(listId);
if (list.userId !== this.userId) {
throw new Meteor.Error('list.unauthorized',
'This list doesn\'t belong to you.');
}
return Lists.find(listId, {
projection: {
name: 1,
incompleteCount: 1,
userId: 1
}
});
});
// #2: Good! When the owner of the list changes, the old owner won't see it anymore
Meteor.publish('list', function (listId) {
check(listId, String);
return Lists.find({
_id: listId,
userId: this.userId
}, {
projection: {
name: 1,
incompleteCount: 1,
userId: 1
}
});
});在第一个示例中,如果所选列表中的 userId 属性发生更改,发布中的查询仍将返回数据,因为初始安全检查不会重新运行。在第二个示例中,我们通过将安全检查放在返回的查询本身中解决了这个问题。
¥In the first example, if the userId property on the selected list changes, the query in the publication will still return the data, since the security check in the beginning will not re-run. In the second example, we have fixed this by putting the security check in the returned query itself.
遗憾的是,并非所有公开的包都像上面的例子那样容易保护。如需了解更多关于如何使用 reywood:publish-composite 处理发布内容中的响应式更改的技巧,请参阅 数据加载文章。
¥Unfortunately, not all publications are as simple to secure as the example above. For more tips on how to use reywood:publish-composite to handle reactive changes in publications, see the data loading article.
传递选项
¥Passing options
对于某些应用(例如分页),你需要将选项传递给发布,以控制诸如应向客户端发送多少个文档之类的设置。对于此特定情况,还有一些额外的注意事项。
¥For certain applications, for example pagination, you'll want to pass options into the publication to control things like how many documents should be sent to the client. There are some extra considerations to keep in mind for this particular case.
传递限制:如果你要从客户端传递查询的
limit选项,请确保设置最大限制。否则,恶意客户端可能会一次请求过多文档,从而导致性能问题。¥Passing a limit: In the case where you are passing the
limitoption of the query from the client, make sure to set a maximum limit. Otherwise, a malicious client could request too many documents at once, which could raise performance issues.传递过滤器:如果你希望传递要过滤的字段(例如,在搜索查询的情况下),请确保使用 MongoDB
$and将来自客户端的过滤器与客户端应该有权查看的文档进行交集匹配。此外,你应该将客户端可用于过滤的键值加入白名单。 - 如果客户端可以过滤秘密数据,则可以运行搜索来查找这些数据。¥Passing in a filter: If you want to pass fields to filter on because you don't want all of the data, for example in the case of a search query, make sure to use MongoDB
$andto intersect the filter coming from the client with the documents that client should be allowed to see. Also, you should whitelist the keys that the client can use to filter - if the client can filter on secret data, it can run a search to find out what that data is.传递字段:如果你希望客户端能够决定要获取集合中的哪些字段,请确保将这些字段与客户端有权查看的字段进行交集匹配,以免意外地将敏感数据发送给客户端。
¥Passing in fields: If you want the client to be able to decide which fields of the collection should be fetched, make sure to intersect that with the fields that client is allowed to see, so that you don't accidentally send secret data to the client.
总之,你应该确保从客户端传递给发布的任何选项只能限制请求的数据,而不能扩展数据。
¥In summary, you should make sure that any options passed from the client to a publication can only restrict the data being requested, rather than extending it.
服务文件
¥Served files
发布并非客户端从服务器获取数据的唯一途径。应用服务器提供的源代码文件和静态资源集也可能包含敏感数据:
¥Publications are not the only place the client gets data from the server. The set of source code files and static assets that are served by your application server could also potentially contain sensitive data:
攻击者可以分析业务逻辑以发现漏洞。
¥Business logic an attacker could analyze to find weak points.
竞争对手可能窃取的秘密算法。
¥Secret algorithms that a competitor could steal.
密钥 API。
¥Secret API keys.
Secret 服务器代码
¥Secret server code
虽然应用的客户端代码必然可以被浏览器访问,但每个应用在服务器端都会有一些你不想与外界共享的秘密代码。
¥While the client-side code of your application is necessarily accessible by the browser, every application will have some secret code on the server that you don't want to share with the world.
应用中的秘密业务逻辑应位于仅在服务器端加载的代码中。这意味着它位于应用的 server/ 目录中,位于仅在服务器端包含的包中,或者位于仅在服务器端加载的包内的某个文件中。
¥Secret business logic in your app should be located in code that is only loaded on the server. This means it is in a server/ directory of your app, in a package that is only included on the server, or in a file inside a package that was loaded only on the server.
如果你的应用中有一个包含敏感业务逻辑的 Meteor 方法,你可能需要将该方法拆分成两个函数。 - 乐观的 UI 部分将在客户端运行,而秘密部分将在服务器端运行。大多数情况下,将整个方法放在服务器上并不能带来最佳的用户体验。我们来看一个例子,假设你有一个用于计算游戏中玩家 MMR(排名)的秘密算法:
¥If you have a Meteor Method in your app that has secret business logic, you might want to split the Method into two functions - the optimistic UI part that will run on the client, and the secret part that runs on the server. Most of the time, putting the entire Method on the server doesn't result in the best user experience. Let's look at an example, where you have a secret algorithm for calculating someone's MMR (ranking) in a game:
js
// In a server-only file, for example /imports/server/mmr.js
export const MMR = {
updateWithSecretAlgorithm(userId) {
// your secret code here
}
}js
// In a file loaded on client and server
Meteor.users.methods.updateMMR = new createMethod({
name: 'Meteor.users.methods.updateMMR',
validate: null,
run() {
if (this.isSimulation) {
// Simulation code for the client (optional)
} else {
const { MMR } = require('/imports/server/mmr.js');
MMR.updateWithSecretAlgorithm(this.userId);
}
}
});警告
请注意,虽然该方法在客户端定义,但实际的秘密逻辑只能从服务器访问,并且代码不会包含在客户端包中。请记住,if (Meteor.isServer) 和 if (!this.isSimulation) 代码块中的代码仍然会发送到客户端,只是不会被执行。所以不要在其中放置任何秘密代码。
¥Note that while the Method is defined on the client, the actual secret logic is only accessible from the server and the code will not be included in the client bundle. Keep in mind that code inside if (Meteor.isServer) and if (!this.isSimulation) blocks is still sent to the client, it is just not executed. So don't put any secret code in there.
绝对不能将密钥存储在源代码中,下一节将讨论如何处理密钥。
¥Secret API keys should never be stored in your source code at all, the next section will talk about how to handle them.
保护 API 密钥
¥Securing API keys
每个应用都将拥有一些密钥或密码:
¥Every app will have some secret API keys or passwords:
你的数据库密码。
¥Your database password.
外部 API 的 API 密钥。
¥API keys for external APIs.
这些文件绝不应该作为应用源代码的一部分存储在版本控制系统中,因为开发者可能会将代码复制到意想不到的地方,并忘记其中包含密钥。你可以将密钥分别保存在 Dropbox、LastPass 或其他服务中,然后在需要部署应用时引用它们。
¥These should never be stored as part of your app's source code in version control, because developers might copy code around to unexpected places and forget that it contains secret keys. You can keep your keys separately in Dropbox, LastPass, or another service, and then reference them when you need to deploy the app.
你可以通过配置文件或环境变量将设置传递给你的应用。大多数应用设置应该放在 JSON 文件中,并在启动应用时传入。你可以通过传递 --settings 标志来使用设置文件启动你的应用:
¥You can pass settings to your app through a settings file or an environment variable. Most of your app settings should be in JSON files that you pass in when starting your app. You can start your app with a settings file by passing the --settings flag:
sh
# Pass development settings when running your app locally
meteor --settings development.json
# Pass production settings when deploying your app to Galaxy
meteor deploy myapp.com --settings production.json以下是包含一些 API 密钥的设置文件示例:
¥Here's what a settings file with some API keys might look like:
js
{
"facebook": {
"appId": "12345",
"secret": "1234567"
}
}在你的应用 JavaScript 代码中,可以通过变量 Meteor.settings 访问这些设置。
¥In your app's JavaScript code, these settings can be accessed from the variable Meteor.settings.
¥Read more about managing keys and settings in the Deployment article.
客户端设置
¥Settings on the client
在大多数情况下,你配置文件中的 API 密钥仅供服务器使用,并且默认情况下,通过 --settings 传递的数据也仅在服务器端可用。但是,如果你将数据放在名为 public 的特殊键下,客户端就可以访问这些数据。例如,如果你需要从客户端发起 API 调用,并且不介意用户知道该密钥,那么你可能需要这样做。公共设置将在客户端的 Meteor.settings.public 下可用。
¥In most normal situations, API keys from your settings file will only be used by the server, and by default the data passed in through --settings is only available on the server. However, if you put data under a special key called public, it will be available on the client. You might want to do this if, for example, you need to make an API call from the client and are OK with users knowing that key. Public settings will be available on the client under Meteor.settings.public.
切勿在设置文件的公共属性中存储重要信息
¥Never store valuable information in public property in settings file
如果你希望客户端可以访问设置文件中的某些属性,但切勿在 public 属性中放置任何有价值的信息,这是可以的。你可以将其显式存储在 private 属性下,也可以将其存储在单独的 property 中。在 Meteor 中,任何不在 public 目录下的属性默认都被视为私有属性。
¥It's ok if you want to make some properties of your settings file accessible to the client but never put any valuable information inside the public property. Either explicity store it under private property or in its own property. Any property that's not under public is treated as private by default in Meteor.
javascript
{
"public": {"publicKey": "xxxxx"},
"private": {"privateKey": "xxxxx"}
}or
javascript
{
"public": {"publicKey": "xxxxx"},
"privateKey": "xxxxx"
}示例:OAuth API 密钥
¥Example: API keys for OAuth
为了让 accounts-facebook 包识别这些键值,你需要将它们添加到数据库中的服务配置集合中。以下是实现方法:
¥For the accounts-facebook package to pick up these keys, you need to add them to the service configuration collection in the database. Here's how you do that:
首先,添加 service-configuration 包:
¥First, add the service-configuration package:
sh
meteor add service-configuration然后,使用私有设置对 ServiceConfiguration 集合进行 upsert 操作:
¥Then, upsert into the ServiceConfiguration collection using private settings:
js
ServiceConfiguration.configurations.upsert({
service: "facebook"
}, {
$set: {
appId: Meteor.settings.private.facebook.appId,
loginStyle: "popup",
secret: Meteor.settings.private.facebook.secret
}
});现在,accounts-facebook 将能够找到 API 密钥,并且 Facebook 登录将正常工作。
¥Now, accounts-facebook will be able to find that API key and Facebook login will work properly.
SSL
本节内容很短,但值得在目录中单独列出。
¥This is a very short section, but it deserves its own place in the table of contents.
每个处理用户数据的生产环境 Meteor 应用都应该使用 SSL 运行。
¥Every production Meteor app that handles user data should run with SSL.
是的,Meteor 会在客户端对你的密码或登录令牌进行哈希处理,然后再通过网络发送,但这只能防止攻击者破解你的密码。 - 这并不能阻止他们以你的身份登录,因为他们可以将哈希密码发送到服务器进行登录!无论如何,登录都需要客户端向服务器发送敏感数据,而确保数据传输安全的唯一方法是使用 SSL。请注意,在普通的 HTTP Web 应用中使用 cookie 进行身份验证时也存在同样的问题,因此任何需要可靠识别用户的应用都应该使用 SSL。
¥Yes, Meteor does hash your password or login token on the client before sending it over the wire, but that only prevents an attacker from figuring out your password - it doesn't prevent them from logging in as you, since they could send the hashed password to the server to log in! No matter how you slice it, logging in requires the client to send sensitive data to the server, and the only way to secure that transfer is by using SSL. Note that the same issue is present when using cookies for authentication in a normal HTTP web application, so any app that needs to reliably identify users should be running on SSL.
设置 SSL
¥Setting up SSL
在 Galaxy 中,SSL 配置是自动的。请参阅 Galaxy 上的 SSL 帮助文章。
¥On Galaxy, configuration of SSL is automatic. See the help article about SSL on Galaxy.
如果你运行的是自己的 infrastructure,则可以通过配置代理 Web 服务器来设置 SSL,有几种方法。查看文章:Josh Owens 谈 SSL 和 Meteor、Meteorpedia 上的 SSL 和 Digital Ocean 教程(含 Nginx 配置)。
¥If you are running on your own infrastructure, there are a few options for setting up SSL, mostly through configuring a proxy web server. See the articles: Josh Owens on SSL and Meteor, SSL on Meteorpedia, and Digital Ocean tutorial with an Nginx config.
强制使用 SSL
¥Forcing SSL
一般来说,所有生产环境的 HTTP 请求都应该通过 HTTPS 发送,所有 WebSocket 数据都应该通过 WSS 发送。
¥Generally speaking, all production HTTP requests should go over HTTPS, and all WebSocket data should be sent over WSS.
最好在处理 SSL 证书和终止的平台上处理从 HTTP 到 HTTPS 的重定向。
¥It's best to handle the redirection from HTTP to HTTPS on the platform which handles the SSL certificates and termination.
在 Galaxy 上,在应用 "设置" 选项卡的 "域名和加密" 部分中,针对特定域启用 "强制使用 HTTPS" 设置。
¥On Galaxy, enable the "Force HTTPS" setting on a specific domain in the "Domains & Encryption" section of the application's "Settings" tab.
其他部署可能具有控制面板选项,或者需要在代理服务器(例如 HAProxy、nginx 等)上手动配置。上面链接的文章对此提供了一些帮助。
¥Other deployments may have control panel options or may need to be manually configured on the proxy server (e.g. HAProxy, nginx, etc.). The articles linked above provide some assistance on this.
如果某个平台不支持此功能,可以将 force-ssl 包添加到项目中,Meteor 将尝试根据 x-forwarded-for 标头的存在情况进行智能重定向。
¥In the event that a platform does not offer the ability to configure this, the force-ssl package can be added to the project and Meteor will attempt to intelligently redirect based on the presence of the x-forwarded-for header.
HTTP 标头
¥HTTP Headers
HTTP 标头可用于提升应用的安全性,虽然并非万无一失,但可以帮助用户缓解常见的攻击。
¥HTTP headers can be used to improve the security of apps, although these are not a silver bullet, they will assist users in mitigating more common attacks.
推荐:Helmet
¥Recommended: Helmet
虽然有很多优秀的开源解决方案可用于设置 HTTP 标头,但 Meteor 推荐使用 Helmet。Helmet 是一组包含 12 个小型中间件函数的集合,用于设置 HTTP 标头。
¥Although there are many great open source solutions for setting HTTP headers, Meteor recommends Helmet. Helmet is a collection of 12 smaller middleware functions that set HTTP headers.
首先,安装 Helmet。
¥First, install helmet.
js
meteor npm install helmet --save默认情况下,可以使用 Helmet 设置各种 HTTP 标头(参见上面的链接)。这些文件是缓解常见攻击的良好起点。要使用默认标头,用户应在服务器端 Meteor 启动代码的任何位置使用以下代码。
¥By default, Helmet can be used to set various HTTP headers (see link above). These are a good starting point for mitigating common attacks. To use the default headers, users should use the following code anywhere in their server side meteor startup code.
注意:Meteor 尚未对每个标头进行全面的兼容性测试。仅测试了以下列出的标头。
¥Note: Meteor has not extensively tested each header for compatibility with Meteor. Only headers listed below have been tested.
js
// With other import statements
import helmet from "helmet";
// Within server side Meter.startup()
WebApp.handlers.use(helmet())Meteor 建议用户至少设置以下标头。请注意,以下代码示例仅适用于 Helmet。
¥At a minimum, Meteor recommends users to set the following headers. Note that code examples shown below are specific to Helmet.
内容安全策略 (CSP)
¥Content Security Policy
注意:内容安全策略未使用 Helmet 的默认标头配置进行配置。
¥Note: Content Security Policy is not configured using Helmet's default header configuration.
根据 MDN 的解释,内容安全策略 (CSP) 是一种额外的安全层,有助于检测和缓解某些类型的攻击,包括跨站脚本攻击 (XSS) 和数据注入攻击。这些攻击可用于各种目的,从数据窃取到网站篡改或恶意软件传播。
¥From MDN, Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, including Cross Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft to site defacement or distribution of malware.
建议用户使用内容安全策略 (CSP) 来保护其应用免受第三方访问。CSP 有助于控制资源加载到应用中的方式。
¥It is recommended that users use CSP to protect their apps from access by third parties. CSP assists to control how resources are loaded into your application.
默认情况下,Meteor 建议允许使用不安全的内联脚本和样式,因为许多应用通常使用它们进行分析等操作。不安全的 eval 操作被禁止,唯一允许的内容源是同源或数据,但 connect 除外,它允许任何内容源(因为 Meteor 应用会与许多不同的源建立 WebSocket 连接)。浏览器也会被告知不要嗅探已声明内容类型之外的内容类型。
¥By default, Meteor recommends unsafe inline scripts and styles are allowed, since many apps typically use them for analytics, etc. Unsafe eval is disallowed, and the only allowable content source is same origin or data, except for connect which allows anything (since meteor apps make websocket connections to a lot of different origins). Browsers will also be told not to sniff content types away from declared content types.
js
// With other import statements
import helmet from "helmet";
// Within server side Meter.startup()
WebApp.handlers.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
connectSrc: ["*"],
imgSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
}
})
);Helmet 支持大量指令,用户应根据自身需求进一步自定义其 CSP(内容安全策略)。更多详细信息,请阅读以下指南:内容安全策略 (CSP)。CSP 可能比较复杂,因此有一些优秀的工具可以提供帮助,例如 Mozilla 的 Google CSP 评估器、Report-URI 的 CSP 构建器、CSP 文档 和 CSPValidator。
¥Helmet supports a large number of directives, users should further customise their CSP based on their needs. For more detail please read the following guide: Content Security Policy. CSP can be complex, so in addition there are some excellent tools out there to help, including Google's CSP Evaluator, Report-URI's CSP Builder, CSP documentation from Mozilla and CSPValidator.
以下示例展示了生产 Meteor 应用中使用的潜在 CSP 和其他安全标头。根据你的设置和使用场景,此配置可能需要自定义。
¥The following example presents a potential CSP and other Security Headers used in a Production Meteor Application. This configuration may require customization, depending on your setup and use-cases.
javascript
/* global __meteor_runtime_config__ */
import { Meteor } from 'meteor/meteor'
import { WebApp } from 'meteor/webapp'
import { Autoupdate } from 'meteor/autoupdate'
import { check } from 'meteor/check'
import crypto from 'crypto'
import helmet from 'helmet'
const self = '\'self\''
const data = 'data:'
const unsafeEval = '\'unsafe-eval\''
const unsafeInline = '\'unsafe-inline\''
const allowedOrigins = Meteor.settings.allowedOrigins
// create the default connect source for our current domain in
// a multi-protocol compatible way (http/ws or https/wss)
const url = Meteor.absoluteUrl()
const domain = url.replace(/http(s)*:\/\//, '').replace(/\/$/, '')
const s = url.match(/(?!=http)s(?=:\/\/)/) ? 's' : ''
const usesHttps = s.length > 0
const connectSrc = [
self,
`http${s}://${domain}`,
`ws${s}://${domain}`
]
// Prepare runtime config for generating the sha256 hash
// It is important, that the hash meets exactly the hash of the
// script in the client bundle.
// Otherwise the app would not be able to start, since the runtimeConfigScript
// is rejected __meteor_runtime_config__ is not available, causing
// a cascade of follow-up errors.
const runtimeConfig = Object.assign(__meteor_runtime_config__, Autoupdate, {
// the following lines may depend on, whether you called Accounts.config
// and whether your Meteor app is a "newer" version
accountsConfigCalled: true,
isModern: true
})
// add client versions to __meteor_runtime_config__
Object.keys(WebApp.clientPrograms).forEach(arch => {
__meteor_runtime_config__.versions[arch] = {
version: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].version(),
versionRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionRefreshable(),
versionNonRefreshable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionNonRefreshable(),
// comment the following line if you use Meteor < 2.0
versionReplaceable: Autoupdate.autoupdateVersion || WebApp.clientPrograms[arch].versionReplaceable()
}
})
const runtimeConfigScript = `__meteor_runtime_config__ = JSON.parse(decodeURIComponent("${encodeURIComponent(JSON.stringify(runtimeConfig))}"))`
const runtimeConfigHash = crypto.createHash('sha256').update(runtimeConfigScript).digest('base64')
const helpmentOptions = {
contentSecurityPolicy: {
blockAllMixedContent: true,
directives: {
defaultSrc: [self],
scriptSrc: [
self,
// Remove / comment out unsafeEval if you do not use dynamic imports
// to tighten security. However, if you use dynamic imports this line
// must be kept in order to make them work.
unsafeEval,
`'sha256-${runtimeConfigHash}'`
],
childSrc: [self],
// If you have external apps, that should be allowed as sources for
// connections or images, your should add them here
// Call helmetOptions() without args if you have no external sources
// Note, that this is just an example and you may configure this to your needs
connectSrc: connectSrc.concat(allowedOrigins),
fontSrc: [self, data],
formAction: [self],
frameAncestors: [self],
frameSrc: ['*'],
// This is an example to show, that we can define to show images only
// from our self, browser data/blob and a defined set of hosts.
// Configure to your needs.
imgSrc: [self, data, 'blob:'].concat(allowedOrigins),
manifestSrc: [self],
mediaSrc: [self],
objectSrc: [self],
// these are just examples, configure to your needs, see
// https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
sandbox: [
// allow-downloads-without-user-activation // experimental
'allow-forms',
'allow-modals',
// 'allow-orientation-lock',
// 'allow-pointer-lock',
// 'allow-popups',
// 'allow-popups-to-escape-sandbox',
// 'allow-presentation',
'allow-same-origin',
'allow-scripts',
// 'allow-storage-access-by-user-activation ', // experimental
// 'allow-top-navigation',
// 'allow-top-navigation-by-user-activation'
],
styleSrc: [self, unsafeInline],
workerSrc: [self, 'blob:']
}
},
// see the helmet documentation to get a better understanding of
// the following configurations and settings
strictTransportSecurity: {
maxAge: 15552000,
includeSubDomains: true,
preload: false
},
referrerPolicy: {
policy: 'no-referrer'
},
expectCt: {
enforce: true,
maxAge: 604800
},
frameguard: {
action: 'sameorigin'
},
dnsPrefetchControl: {
allow: false
},
permittedCrossDomainPolicies: {
permittedPolicies: 'none'
}
}
// We assume, that we are working on a localhost when there is no https
// connection available.
// Run your project with --production flag to simulate script-src hashing
if (!usesHttps && Meteor.isDevelopment) {
delete helpmentOptions.contentSecurityPolicy.blockAllMixedContent;
helpmentOptions.contentSecurityPolicy.directives.scriptSrc = [
self,
unsafeEval,
unsafeInline,
];
}
// finally pass the options to helmet to make them apply
helmet(helpmentOptions)X-Frame-Options
注意:X-Frame Options 标头使用 Helmet 的默认标头配置进行配置。
¥Note: The X-Frame Options header is configured using Helmet's default header configuration.
根据 MDN 的说明,可以使用 X-Frame-Options HTTP 响应头来指示浏览器是否应该以 <frame>、<iframe> 或 <object> 格式渲染页面。网站可以使用此功能来避免点击劫持攻击,方法是确保其内容不被嵌入到其他网站中。
¥From MDN, the X-Frame-Options HTTP response header can be used to indicate whether or not a browser should be allowed to render a page in a <frame>, <iframe> or <object>. Sites can use this to avoid clickjacking attacks, by ensuring that their content is not embedded into other sites.
Meteor 建议用户仅针对同源配置 X-Frame-Options 标头。这告诉浏览器阻止你的网页被放入 iframe 中。通过使用此配置,你可以设置策略,只有与你的应用位于同一源的网页才能嵌入你的应用。
¥Meteor recommend users configure the X-Frame-Options header for same origin only. This tells browsers to prevent your webpage from being put in an iframe. By using this config, you will set your policy where only web pages on the same origin as your app can frame your app.
使用 Helmet 时,Frameguard 会设置 X-Frame-Options 标头。
¥With Helmet, Frameguard sets the X-Frame-Options header.
js
// With other import statements
import helmet from "helmet";
// Within server side Meter.startup()
WebApp.handlers.use(helmet.frameguard()); // defaults to sameorigin更多详细信息,请阅读以下指南:Frameguard。
¥For more detail please read the following guide: Frameguard.
安全检查清单
¥Security checklist
这是一组用于检查应用的要点,这些要点可能会捕获常见错误。但是,这并非完整列表 - 如果你发现遗漏,请告知我们或提交 pull request!
¥This is a collection of points to check about your app that might catch common errors. However, it's not an exhaustive list yet---if we missed something, please let us know or file a pull request!
确保你的应用没有
insecure或autopublish包。¥Make sure your app doesn't have the
insecureorautopublishpackages.验证所有方法和发布参数,并包含
audit-argument-checks以自动检查。¥Validate all Method and publication arguments, and include the
audit-argument-checksto check this automatically.对你的应用应用速率限制以防止 DDoS 攻击。
¥Apply rate limiting to your application to prevent DDoS attacks.
¥Use Methods instead of client-side insert/update/remove and allow/deny.
在发布中使用特定的选择器和 筛选字段。
¥Use specific selectors and filter fields in publications.
除非你非常清楚自己在做什么,否则请勿使用 在 Blaze 中包含原始 HTML。
¥Don't use raw HTML inclusion in Blaze unless you really know what you are doing.
¥Make sure secret API keys and passwords aren't in your source code.
切勿在 Meteor 设置文件的
public属性中存储重要信息。¥Never store valuable information in
publicproperty of Meteor settings file.保护数据,而非用户界面。 - 重定向到客户端路由并不会提高安全性,这只是一个不错的用户体验功能。
¥Secure the data, not the UI - redirecting away from a client-side route does nothing for security, it's a nice UX feature.
永远不要信任客户端传递的用户 ID。 在 Methods 和 publications 中使用
this.userId。¥Don't ever trust user IDs passed from the client. Use
this.userIdinside Methods and publications.使用 Helmet 设置安全的 HTTP 标头,但请注意并非所有浏览器都支持 Helmet,因此它为使用现代浏览器的用户提供了一层额外的安全保障。
¥Set up secure HTTP headers using Helmet, but know that not all browsers support it so it provides an extra layer of security to users with modern browsers.
Meteor 本质上是一个 Node.js 应用,因此请务必遵循 最佳实践 以确保最大程度的安全性。
¥At the end of the day, Meteor is a Node.js app so make sure to also follow the best practises to ensure maximum security.
应用保护
¥App Protection
Galaxy Hosting 上的应用保护是我们代理服务器层的一项功能,它位于发送到你应用的每个请求之前。这意味着所有跨服务器的请求都会被分析并根据预期限制进行衡量。这将有助于防止旨在使服务器过载并导致你的应用无法处理合法请求的 DoS 和 DDoS 攻击。
¥App Protection on Galaxy Hosting is a feature in our proxy server layer that sits in front of every request to your application. This means that all requests across servers are analyzed and measured against expected limits. This will help protect against DoS and DDoS attacks that aimed to overload servers and make your app unavailable for legitimate requests.
如果某种类型的请求被判定为滥用请求(我们不会详细说明如何判定),我们将停止向你的应用发送此类请求,并开始返回 HTTP 429(请求过多)错误。*
¥If a type of request is classified as abusive (we’re not going to go into the specifics as to how we determine this), we will stop sending these requests to your app, and we start to return HTTP 429 (Too Many Requests).*
虽然并非所有攻击都能预防,但我们的应用保护功能,以及服务器前端的标准 AWS 保护,将为未来部署到 Galaxy 的所有应用提供更高级别的安全性。
¥Although not all attacks are preventable, our App Protection functionality, along with standard AWS protection in front of our servers, will provide a greater level of security for all applications deployed to Galaxy moving forward.
为了提高安全性,最好将你的应用配置为限制通过 WebSocket 接收的消息数量,因为我们的代理服务器仅在首次连接中起作用,连接建立后不会处理 WebSocket 消息。Meteor 已提供 DDP 速率限制器配置,了解更多信息,请参阅 此处。
¥For additional security, it is best to configure your app to limit the messages received via WebSockets, as our proxy servers are only acting in the first connection and not in the WebSocket messages after the connection is established. Meteor has the DDP Rate Limiter configuration already available, find out more here.

