安全

如何保护你的 Meteor 应用。

阅读本指南后,您将了解

  1. Meteor 应用的安全范围。
  2. 如何保护 Meteor 方法、发布和源代码。
  3. 在开发和生产环境中存储密钥的位置。
  4. 在审核应用时如何遵循安全检查清单。
  5. Galaxy Hosting 中的应用保护功能如何工作。

简介

保护 Web 应用的关键在于理解安全域并了解这些域之间的攻击面。在 Meteor 应用中,情况非常简单

  1. 在服务器上运行的代码是可信的。
  2. 其他所有内容:在客户端上运行的代码、通过方法和发布参数发送的数据等,都是不可信的。

在实践中,这意味着您应该在这两个域之间的边界上执行大部分安全和验证操作。简单来说

  1. 验证和检查来自客户端的所有输入。
  2. 不要将任何秘密信息泄露给客户端。

概念:攻击面

由于 Meteor 应用通常以将客户端和服务器代码组合在一起的方式编写,因此特别需要注意哪些代码在客户端上运行,哪些代码在服务器上运行,以及边界在哪里。以下是需要在 Meteor 应用中进行安全检查的完整列表

  1. 方法:通过方法参数传入的任何数据都需要进行验证,并且方法不应返回用户无权访问的数据。
  2. 发布:通过发布参数传入的任何数据都需要进行验证,并且发布不应返回用户无权访问的数据。
  3. 服务文件:您应该确保服务到客户端的任何源代码或配置文件都不包含秘密数据。

以下每个要点都将在下面有自己的章节。

避免使用 allow/deny

在本指南中,我们将坚定地认为使用 allowdeny 直接从客户端运行 MongoDB 查询不是一个好主意。主要原因是很难遵循上面概述的原则。验证所有可能的 MongoDB 运算符的空间非常困难,并且随着 MongoDB 的新版本发布,这些运算符可能会随着时间推移而增长。

已经有几篇文章讨论了从客户端接受 MongoDB 更新运算符的潜在陷阱,特别是 Discover Meteor 博客上的 允许和拒绝安全挑战 及其 结果

鉴于上述几点,我们建议所有 Meteor 应用都应使用方法来接受来自客户端的数据输入,并尽可能严格地限制每个方法接受的参数。

以下是一个添加到服务器代码中的代码片段,用于禁用集合上的客户端更新。这将确保您的应用的任何其他部分都不能使用 allow

// Deny all client-side updates on the Lists collection
Lists.deny({
  insert() { return true; },
  update() { return true; },
  remove() { return true; },
});

方法

方法是 Meteor 服务器接受来自外部世界输入和数据的方式,因此它们自然成为安全方面最重要的主题。如果您没有正确保护方法,用户最终可能会以意想不到的方式修改您的数据库——编辑其他人的文档、删除数据或弄乱数据库模式导致应用崩溃。

验证所有参数

如果您假设输入是正确的,那么编写干净的代码会容易得多,因此在运行任何实际业务逻辑之前验证所有方法参数非常有价值。您不希望有人传递您不期望的数据类型并导致意外行为。

请考虑,如果您正在为方法编写单元测试,则需要测试所有可能输入到方法中的类型;验证参数限制了您需要进行单元测试的输入空间,从而减少了您需要编写的代码量。它还有一个额外的优点,即它是自文档化的;其他人可以查看代码以了解方法正在查找哪种参数。

举个例子,以下情况说明了不检查参数可能带来的灾难性后果

Meteor.methods({
  removeWidget(id) {
    if (! this.userId) {
      throw new Meteor.Error('removeWidget.unauthorized');
    }

    Widgets.remove(id);
  }
});

如果有人传递了一个非 ID 选择器,例如 {},他们最终将删除整个集合。

mdg:validated-method

为了帮助您编写能够详尽验证其参数的良好方法,我们编写了一个方法包装程序包,用于强制执行参数验证。在 方法文章 中详细了解如何使用它。本文其余代码示例将假定您正在使用此包。如果您没有使用,您仍然可以应用相同的原则,但代码看起来会略有不同。

不要从客户端传递 userId

每个 Meteor 方法内部的 this 上下文包含一些关于当前连接的有用信息,其中最有用的是 this.userId。此属性由 DDP 登录系统管理,并且框架本身保证其安全性遵循广泛使用的最佳实践。

鉴于当前用户的用户 ID 可通过此上下文获得,您永远不要将当前用户的 ID 作为参数传递给方法。这将允许您的应用的任何客户端传递他们想要的任何用户 ID。让我们来看一个例子

// #1: Bad! The client could pass any user ID and set someone else's name
setName({ userId, newName }) {
  Meteor.users.update(userId, {
    $set: { name: newName }
  });
}

// #2: Good, the client can only set the name on the currently logged in user
setName({ newName }) {
  Meteor.users.update(this.userId, {
    $set: { name: newName }
  });
}

您应该仅在以下情况下将任何用户 ID 作为参数传递

  1. 这是一种仅供管理员用户访问的方法,他们被允许编辑其他用户。请参阅有关 用户角色 的部分,了解如何检查用户是否在特定角色中。
  2. 此方法不修改其他用户,而是将其用作目标;例如,它可以是用于发送私人消息或将用户添加为好友的方法。

每个动作一个方法

使应用安全化的最佳方法是了解所有可能来自不受信任来源的输入,并确保所有输入都得到正确处理。了解哪些输入可以来自客户端的最简单方法是将其限制在尽可能小的空间内。这意味着您的所有方法都应该是特定的操作,而不应该包含大量以显著方式改变行为的选项。最终目标是,您可以查看应用中的每个方法并验证或测试其安全性。以下来自 Todos 示例应用的安全方法示例

export const makePrivate = new ValidatedMethod({
  name: 'lists.makePrivate',
  validate: new SimpleSchema({
    listId: { type: String }
  }).validator(),
  run({ listId }) {
    if (!this.userId) {
      throw new Meteor.Error('lists.makePrivate.notLoggedIn',
        'Must be logged in to make private lists.');
    }

    const list = Lists.findOne(listId);

    if (list.isLastPublicList()) {
      throw new Meteor.Error('lists.makePrivate.lastPublicList',
        'Cannot make the last public list private.');
    }

    Lists.update(listId, {
      $set: { userId: this.userId }
    });

    Lists.userIdDenormalizer.set(listId, this.userId);
  }
});

您可以看到此方法执行非常具体的操作——它将单个列表设为私有。另一种方法是使用名为 setPrivacy 的方法,它可以将列表设置为私有或公开,但事实证明,在此特定应用中,这两个相关操作(makePrivatemakePublic)的安全注意事项非常不同。通过将操作拆分为不同的方法,我们使每个操作都更加清晰。从上面的方法定义中可以明显看出我们接受哪些参数、执行哪些安全检查以及对数据库执行哪些操作。

但是,这并不意味着您不能在方法中添加任何灵活性。让我们来看一个例子

Meteor.users.methods.setUserData = new ValidatedMethod({
  name: 'Meteor.users.methods.setUserData',
  validate: new SimpleSchema({
    fullName: { type: String, optional: true },
    dateOfBirth: { type: Date, optional: true },
  }).validator(),
  run(fieldsToSet) {
    Meteor.users.update(this.userId, {
      $set: fieldsToSet
    });
  }
});

以上方法很棒,因为您可以灵活地使用一些可选字段,并且只传递您想要更改的字段。特别是,使此方法成为可能的原因是设置全名和出生日期的安全注意事项相同——我们不必对要设置的不同字段进行不同的安全检查。请注意,在 MongoDB 上生成 $set 查询非常重要——我们永远不应该按原样从客户端获取 MongoDB 运算符,因为它们很难验证并且可能导致意外副作用。

重构以重用安全规则

您可能会遇到应用中的许多方法具有相同安全检查的情况。这可以通过将安全检查分解到单独的模块中、包装方法主体或扩展 Mongo.Collection 类以在服务器上的 insertupdateremove 实现中执行安全检查来简化。但是,通过特定方法实现客户端-服务器通信仍然是一个好主意,而不是从客户端发送任意 update 运算符,因为恶意客户端无法发送您未测试过的 update 运算符。

速率限制

与 REST 端点一样,Meteor 方法可以从任何地方调用——恶意程序、浏览器控制台中的脚本等。在非常短的时间内发出许多方法调用很容易。这意味着攻击者很容易测试大量不同的输入以找到一个有效的输入。Meteor 对密码登录内置了速率限制,以阻止密码暴力破解,但您需要为其他方法定义速率限制。

在 Todos 示例应用中,我们使用以下代码对所有方法设置基本速率限制

// 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 次。这是一个用户根本不会注意到的速率限制,但可以防止恶意脚本完全用请求淹没服务器。您需要调整限制参数以匹配应用的需求。

如果您使用的是经过验证的方法,则可以使用 ddp-rate-limiter-mixin

发布

发布是 Meteor 服务器向客户端提供数据的主要方式。虽然在方法中,主要关注的是确保用户无法以意外的方式修改数据库,但在发布中,主要问题是过滤返回的数据,以防止恶意用户访问他们不应该看到的数据。

您无法在渲染层进行安全控制

在像 Ruby on Rails 这样的服务器端渲染框架中,只需不在返回的 HTML 响应中显示敏感数据即可。在 Meteor 中,由于渲染是在客户端完成的,因此 HTML 模板中的 if 语句并不安全;您需要在数据级别进行安全控制,以确保数据永远不会被发送。

方法的相关规则仍然适用

上面关于方法的所有要点也适用于发布。

  1. 使用 check 或 npm simpl-schema 验证所有参数。
  2. 切勿将当前用户 ID 作为参数传递。
  3. 不要使用泛型参数;确保您完全了解您的发布从客户端获取的内容。
  4. 使用速率限制来阻止人们用订阅轰炸您。

始终限制字段

Mongo.Collection#find 具有一个名为 fields 的选项,它允许您过滤获取的文档上的字段。您应该始终在发布中使用此选项,以确保您不会意外地发布秘密字段。

例如,您可以编写一个发布,然后稍后向已发布的集合添加一个秘密字段。现在,发布将把该秘密发送到客户端。如果您在第一次编写发布时过滤每个发布上的字段,那么添加另一个字段不会自动发布它。

// #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}}, {
    fields: {
      name: 1,
      incompleteCount: 1,
      userId: 1
    }
  });
});

如果您发现自己经常重复字段,那么将公共字段的字典提取出来,以便您可以始终对其进行过滤,如下所示

// In the file where Lists is defined
Lists.publicFields = {
  name: 1,
  incompleteCount: 1,
  userId: 1
};

现在您的代码变得稍微简单了一些

Meteor.publish('lists.public', function () {
  return Lists.find({userId: {$exists: false}}, {
    fields: Lists.publicFields
  });
});

发布和 userId

从发布返回的数据通常取决于当前登录的用户,以及可能与该用户相关的一些属性 - 他们是否是管理员,他们是否拥有某个文档等。

发布不是反应式的,并且仅在当前登录的 userId 更改时重新运行,可以通过 this.userId 访问。因此,很容易意外地编写一个在第一次运行时是安全的发布,但不会响应应用程序环境中的更改。让我们来看一个例子

// #1: Bad! If the owner of the list changes, the old owner will still see it
Meteor.publish('list', function (listId) {
  check(listId, String);

  const list = Lists.findOne(listId);

  if (list.userId !== this.userId) {
    throw new Meteor.Error('list.unauthorized',
      'This list doesn\'t belong to you.');
  }

  return Lists.find(listId, {
    fields: {
      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
  }, {
    fields: {
      name: 1,
      incompleteCount: 1,
      userId: 1
    }
  });
});

在第一个示例中,如果所选列表上的 userId 属性发生更改,发布中的查询仍将返回数据,因为开头的安全检查不会重新运行。在第二个示例中,我们通过将安全检查放在返回的查询本身中来解决此问题。

不幸的是,并非所有发布都像上面的示例那样易于保护。有关如何使用 reywood:publish-composite 处理发布中反应式更改的更多提示,请参阅 数据加载文章

传递选项

对于某些应用程序,例如分页,您需要将选项传递到发布中以控制发送到客户端的文档数量等内容。对于这种情况,需要考虑一些额外的事项。

  1. 传递限制:在您从客户端传递查询的 limit 选项的情况下,请确保设置最大限制。否则,恶意客户端可能会一次请求过多的文档,这可能会导致性能问题。
  2. 传递过滤器:如果您想传递要过滤的字段,因为您不希望获得所有数据,例如在搜索查询的情况下,请确保使用 MongoDB $and 将来自客户端的过滤器与客户端应该被允许查看的文档相交。此外,您应该将客户端可以用来过滤的键列入白名单 - 如果客户端可以过滤秘密数据,它可以运行搜索以找出该数据是什么。
  3. 传递字段:如果您希望客户端能够决定要获取集合的哪些字段,请确保将其与客户端被允许查看的字段相交,这样您就不会意外地将秘密数据发送到客户端。

总之,您应该确保从客户端传递到发布的任何选项只能限制被请求的数据,而不是扩展它。

已提供的文件

发布不是客户端从服务器获取数据的唯一地方。应用程序服务器提供的源代码文件和静态资产集也可能包含敏感数据

  1. 攻击者可以分析的业务逻辑以查找弱点。
  2. 竞争对手可能窃取的秘密算法。
  3. 秘密 API 密钥。

秘密服务器代码

虽然应用程序的客户端代码必然可以通过浏览器访问,但每个应用程序都将在服务器上有一些秘密代码,您不希望与全世界共享。

应用程序中的秘密业务逻辑应位于仅在服务器上加载的代码中。这意味着它位于应用程序的 server/ 目录中,位于仅包含在服务器上的包中,或位于仅在服务器上加载的包内的文件中。

如果您的应用程序中有一个包含秘密业务逻辑的 Meteor 方法,您可能希望将该方法拆分为两个函数 - 将在客户端运行的乐观 UI 部分,以及在服务器上运行的秘密部分。大多数情况下,将整个方法放在服务器上不会带来最佳的用户体验。让我们来看一个示例,其中您有一个用于计算游戏中某人 MMR(排名)的秘密算法

// In a server-only file, for example /imports/server/mmr.js
export const MMR = {
  updateWithSecretAlgorithm(userId) {
    // your secret code here
  }
}
// In a file loaded on client and server
Meteor.users.methods.updateMMR = new ValidatedMethod({
  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) 块内的代码仍然会发送到客户端,只是不会执行。因此,不要在其中放置任何秘密代码。

秘密 API 密钥永远不应该存储在您的源代码中,下一节将讨论如何处理它们。

保护 API 密钥

每个应用程序都将有一些秘密 API 密钥或密码

  1. 您的数据库密码。
  2. 外部 API 的 API 密钥。

这些永远不应该作为应用程序源代码的一部分存储在版本控制中,因为开发人员可能会将代码复制到意外的地方并忘记它包含秘密密钥。您可以将密钥单独保存在 DropboxLastPass 或其他服务中,然后在需要部署应用程序时引用它们。

您可以通过设置文件环境变量将设置传递到您的应用程序。您的大多数应用程序设置都应该位于 JSON 文件中,您在启动应用程序时将其传递进去。您可以通过传递 --settings 标志来使用设置文件启动应用程序

# 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 密钥的设置文件示例

{
  "facebook": {
    "appId": "12345",
    "secret": "1234567"
  }
}

在应用程序的 JavaScript 代码中,可以通过变量 Meteor.settings 访问这些设置。

在部署文章中阅读有关管理密钥和设置的更多信息。

客户端上的设置

在大多数正常情况下,设置文件中的 API 密钥仅供服务器使用,并且默认情况下,通过 --settings 传递的数据仅在服务器上可用。但是,如果您将数据放在名为 public 的特殊键下,它将在客户端上可用。例如,如果您需要从客户端进行 API 调用并且可以接受用户知道该密钥,则您可能希望这样做。公共设置将在客户端上的 Meteor.settings.public 下可用。

切勿在设置文件的公共属性中存储有价值的信息

如果您希望设置文件的一些属性可供客户端访问,但绝不要在 public 属性中放置任何有价值的信息,这样做是可以的。要么明确地将其存储在 private 属性下,要么存储在它自己的 property 中。在 Meteor 中,默认情况下,任何不在 public 下的属性都被视为私有属性。

{
"public": {"publicKey": "xxxxx"},
"private": {"privateKey": "xxxxx"}
}

或者

{
"public": {"publicKey": "xxxxx"},
"privateKey": "xxxxx"
}

OAuth 的 API 密钥

为了使 accounts-facebook 包能够获取这些密钥,您需要将其添加到数据库中的服务配置集合中。以下是如何操作

首先,添加 service-configuration

meteor add service-configuration

然后,更新 ServiceConfiguration 集合

ServiceConfiguration.configurations.upsert({
  service: "facebook"
}, {
  $set: {
    appId: Meteor.settings.facebook.appId,
    loginStyle: "popup",
    secret: Meteor.settings.facebook.secret
  }
});

现在,accounts-facebook 将能够找到该 API 密钥,并且 Facebook 登录将正常工作。

SSL

这是一个非常简短的部分,但在目录中应该有它自己的位置。

每个处理用户数据的生产 Meteor 应用程序都应该使用 SSL 运行。

是的,Meteor 会在通过网络发送之前对您的密码或登录令牌进行哈希处理,但这只能防止攻击者找出您的密码 - 它不能阻止他们以您的身份登录,因为他们可以将哈希后的密码发送到服务器以登录!无论您如何处理,登录都需要客户端将敏感数据发送到服务器,而保护该传输的唯一方法是使用 SSL。请注意,在普通 HTTP Web 应用程序中使用 Cookie 进行身份验证时也存在相同的问题,因此任何需要可靠识别用户的应用程序都应该在 SSL 上运行。

设置 SSL

强制使用 SSL

一般来说,所有生产 HTTP 请求都应该通过 HTTPS 进行,所有 WebSocket 数据都应该通过 WSS 发送。

最好在处理 SSL 证书和终止的平台上处理从 HTTP 到 HTTPS 的重定向。

  • Galaxy 上,在应用程序“设置”选项卡的“域名和加密”部分中,为特定域启用“强制 HTTPS”设置。
  • 其他部署可能具有控制面板选项,或者可能需要在代理服务器(例如 HAProxy、nginx 等)上手动配置。上面链接的文章对此提供了一些帮助。

如果平台不提供配置此功能的能力,则可以将 force-ssl 包添加到项目中,并且 Meteor 将尝试根据 x-forwarded-for 标头是否存在来智能地重定向。

HTTP 标头

HTTP 标头可用于提高应用程序的安全性,尽管这些不是灵丹妙药,但它们将帮助用户减轻更常见的攻击。

推荐:Helmet

尽管有很多优秀的开源解决方案可以设置 HTTP 头,但 Meteor 建议使用 Helmet。Helmet 是一个包含 12 个小型中间件函数的集合,用于设置 HTTP 头。

首先,安装 helmet。

  meteor npm install helmet --save

默认情况下,Helmet 可用于设置各种 HTTP 头(请参阅上面的链接)。这些是缓解常见攻击的良好起点。要使用默认头,用户应在其服务器端 Meteor 启动代码中的任何位置使用以下代码。

注意:Meteor 尚未广泛测试每个头与 Meteor 的兼容性。只有下面列出的头已过测试。

// With other import statements
import helmet from "helmet";

// Within server side Meter.startup()
WebApp.connectHandlers.use(helmet())

至少,Meteor 建议用户设置以下头。请注意,下面显示的代码示例特定于 Helmet。

内容安全策略

注意:内容安全策略不是使用 Helmet 的默认头配置进行配置的。

来自 MDN,内容安全策略 (CSP) 是一种额外的安全层,有助于检测和缓解某些类型的攻击,包括跨站点脚本 (XSS) 和数据注入攻击。这些攻击用于从数据窃取到网站篡改或恶意软件分发等各种目的。

建议用户使用 CSP 来保护其应用免受第三方访问。CSP 有助于控制如何将资源加载到您的应用程序中。

默认情况下,Meteor 建议允许不安全的内联脚本和样式,因为许多应用通常将它们用于分析等目的。不允许使用不安全的 eval,并且唯一允许的内容源是同源或数据,除了 connect 允许任何内容(因为 Meteor 应用与许多不同的来源建立 Websocket 连接)。浏览器还将被告知不要将内容类型嗅探到声明的内容类型之外。

// With other import statements
import helmet from "helmet";

// Within server side Meter.startup()
WebApp.connectHandlers.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      connectSrc: ["*"],
      imgSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
    }
  })
);

Helmet 支持大量指令,用户应根据其需求进一步自定义其 CSP。有关更多详细信息,请阅读以下指南:内容安全策略。CSP 可能很复杂,因此还有一些优秀的工具可以提供帮助,包括 Google 的 CSP 评估器Report-URI 的 CSP 生成器、来自 Mozilla 的 CSP 文档CSPValidator

以下示例展示了生产环境 Meteor 应用程序中可能使用的 CSP 和其他安全头。根据您的设置和用例,此配置可能需要自定义。

/* 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://mdn.org.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 的默认头配置进行配置的。

来自 MDN,X-Frame-Options HTTP 响应头可用于指示是否允许浏览器在 <frame><iframe><object> 中呈现页面。网站可以通过确保其内容不被嵌入到其他网站中来使用此功能避免点击劫持攻击。

Meteor 建议用户仅为同源配置 X-Frame-Options 头。这告诉浏览器阻止您的网页放在 iframe 中。通过使用此配置,您将设置策略,只有与您的应用具有相同来源的网页才能框架您的应用。

使用 Helmet,Frameguard 设置 X-Frame-Options 头。

// With other import statements
import helmet from "helmet";

// Within server side Meter.startup()
WebApp.connectHandlers.use(helmet.frameguard());  // defaults to sameorigin

有关更多详细信息,请阅读以下指南:Frameguard

安全检查清单

这是一个关于您的应用的一些需要检查的要点集合,可能会捕获常见的错误。但是,它还不是一个详尽的列表——如果我们遗漏了什么,请告诉我们或提交一个拉取请求!

  1. 确保您的应用没有 insecureautopublish 包。
  2. 验证所有方法和发布参数,并包含 audit-argument-checks 以自动检查此项。
  3. 对您的应用程序应用速率限制以防止 DDoS 攻击。
  4. 拒绝对用户文档上的 profile 字段进行写入。
  5. 使用方法而不是客户端插入/更新/删除和允许/拒绝。
  6. 在发布中使用特定的选择器和 过滤字段
  7. 除非您非常清楚自己在做什么,否则不要使用 Blaze 中的原始 HTML 包含
  8. 确保您的源代码中没有秘密 API 密钥和密码。
  9. 切勿将有价值的信息存储在 Meteor 设置文件的 public 属性中。
  10. 保护数据,而不是 UI - 从客户端路由重定向对安全性没有任何作用,它是一个不错的用户体验功能。
  11. 切勿信任客户端传递的用户 ID。 在方法和发布中使用 this.userId
  12. 使用 Helmet 设置安全 HTTP 头,但请注意,并非所有浏览器都支持它,因此它为使用现代浏览器的用户提供了额外的安全层。
  13. 归根结底,Meteor 是一个 Node.js 应用,因此请确保也遵循 最佳实践 以确保最大安全性。

应用保护

Galaxy 托管上的应用保护是我们代理服务器层中的一个功能,它位于到达您应用程序的每个请求的前面。这意味着所有跨服务器的请求都会被分析并根据预期限制进行衡量。这将有助于防止旨在使服务器过载并使您的应用无法响应合法请求的 DoS 和 DDoS 攻击。

如果某种类型的请求被归类为滥用(我们不会详细说明我们如何确定这一点),我们将停止将这些请求发送到您的应用,并开始返回 HTTP 429(请求过多)。*

虽然并非所有攻击都可以预防,但我们的应用保护功能以及我们服务器前面的标准 AWS 保护将为将来部署到 Galaxy 的所有应用程序提供更高水平的安全性。

为了获得额外的安全性,最好将您的应用配置为限制通过 WebSockets 收到的消息,因为我们的代理服务器仅在第一个连接中起作用,而在建立连接后的 WebSocket 消息中不起作用。Meteor 已经提供了 DDP 速率限制器配置,可以在这里了解更多信息 这里

在 GitHub 上编辑
// 搜索框