保护 Firebase Cloud Function HTTP 端点

Posted

技术标签:

【中文标题】保护 Firebase Cloud Function HTTP 端点【英文标题】:Securing Firebase Cloud Function HTTP Endpoint 【发布时间】:2021-07-17 21:56:11 【问题描述】:

我有一个非常简单的前置网站用例。它有一个联系表格,联系表格的详细信息需要保存到 firebase 数据库以供进一步处理。该网站使用NextJS 构建。我了解 NextJSapi 功能无法使用 Firebase 托管,因此,我倾向于使用 Cloud Functions 设置一个 HTTP 端点,该端点接受表单数据作为 POST 请求并将其保存到实时数据库/Firestore。

但是,我无法找到保护此端点的方法。如何防止普通用户从网站源代码中提取端点 URL 并向该 URL 发送多个请求?我可以让这个端点只响应那个特定的域吗?或者我该如何解决?

或者,我可以直接在应用程序内使用 Firebase SDK 并将数据保存到数据库中,但这需要我将 contacts 集合保持为公开状态以供任何人读取/写入,这又是一个安全风险。

在保持安全性不变的同时解决此问题的更好方法是什么?请注意,由于它是一个公共网站,因此我无法通过 Firebase 获得经过身份验证的用户。

【问题讨论】:

如果您希望您的数据安全,则需要进行身份验证。你不能让它既安全又公开。您当然可以在您的 Firestore 或实时数据库实例中设置一个只写路径,未经身份验证的用户无法读取但可以写入该路径以发布您的表单数据。您可能应该从该方法开始,并根据需要适应 Functions 或 NextJS 后端(提示:您在此处描述的任何内容都不需要这个)。 【参考方案1】:

你的两个选项都可以工作。

    使用带有云功能的其余 api,您可以集成 Google Captcha。

    直接使用数据库,您可以编写数据库规则,每个人都只能添加一个新联系人,而不能读取或编辑它。这仍然不太安全,因为有人可能会填满您的数据库。但是如果有一个非常好的字段验证和重复限制,这将是一个“较小”的问题。

以下是我们在网站上的做法:

处理captcha和联系POST的云函数:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();

const rp = require("request-promise");
const nodemailer = require("nodemailer");
const gmailEmail = encodeURIComponent(functions.config().gmail.email);
const gmailPassword = encodeURIComponent(functions.config().gmail.password);
const mailTransport = nodemailer.createTransport(
  `smtps://$gmailEmail:$gmailPassword@smtp.gmail.com`
);

exports.checkRecaptcha = functions
  .region("europe-west1")
  .https.onRequest((req, res) => 
    const response = req.query.response;
    console.log("recaptcha response", response);
    rp(
      uri: "https://recaptcha.google.com/recaptcha/api/siteverify",
      method: "POST",
      formData: 
        secret: "TOP_SECRET",
        response: response,
      ,
      json: true,
    )
      .then((result) => 
        console.log("recaptcha result", result);
        if (result.success) 
          res.send("You're good to go, human.");
         else 
          res.send("Recaptcha verification failed. Are you a robot?");
        
      )
      .catch((reason) => 
        console.log("Recaptcha request failure", reason);
        res.send("Recaptcha request failed.");
      );
  );

exports.triggerEmail = functions
  .region("europe-west1")
  .https.onRequest((req, res) => 
    if (req.method !== "POST") 
      res.status(400).send("Please send a POST request");
      return;
    

    const values = JSON.parse(req.body);

    const email = "email@company.com";
    const bcc = "email@company.com";

    const mailOptions = 
      subject: "Kontakt von Website",
      text: `Datum: $values.dateTime\n
Name: $`$values.gender $values.firstname $values.lastname`
Firmenname: $values.company ? values.company : ""
Strasse: $values.street ? values.street : ""
Ort: $values.place ? values.place : ""
Land: $values.country ? values.country : ""
PLZ: $values.zip ? values.zip : ""
E-Mail: $values.email ? values.email : ""\n\n
$values.text ? values.text : ""`,
      to: email,
      bcc: bcc,
    ;

    console.log(req.headers.origin);
    if (
      req.headers.origin == "https://www.your_company.com" ||
      req.headers.origin == "http://localhost:3000"
    ) 
      res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
    

    return mailTransport.sendMail(mailOptions).then(() => 
      res.status(200).send("OK");
    );
  );

我们这边的验证码使用反应:

<ReCAPTCHA
                  ref='recaptcha'
                  sitekey='SECRET_KEY'
                  onChange=response => this.setState( response: response )
                />

还有联系人POST拨打:

 fetch('https://URL_TO_YOUR_FUNCTION_THAT_SENDS_THE_EMAIL', 
                    method: 'POST',
                    body: JSON.stringify(
                      dateTime: new Date().toString(),
                      gender: this.state.gender,
                      firstname: this.state.firstname,
                      lastname: this.state.lastname,
                      company: this.state.company,
                      street: this.state.street,
                      place: this.state.place,
                      country: this.state.country,
                      zip: this.state.zip,
                      email: this.state.email,
                      text: this.state.text,
                      isLogistik: isLogistik
                    )
                  )

确保您的验证码设置已设置为我们的第二个云功能进行验证。

【讨论】:

当然,很想看看 Google Captcha 的示例。隐形选项看起来很有趣。 我已经用示例代码更新了答案。【参考方案2】:

这是不可能的。用户可以在网络选项卡中查看他们正在调用的所有 URL。您的无服务器功能必须准备好处理垃圾邮件(例如拒绝恶意或格式错误的请求),但您仍需为 CPU 使用率付费。这是无服务器的最大缺点之一。但是您将节省大量设置服务器和所有麻烦的时间。

您可以做的最好的事情是启用CORS,它仍然不会阻止垃圾邮件,但会在飞行前请求后拒绝请求。虽然只有浏览器遵循 CORS 和 API 客户端,如 postman 或 insomnia 没有

这不能被视为安全威胁,因为这完全取决于您的代码逻辑,但是您需要为使用收费,这就是所涉及的风险。有像 Cloudflare API Shield 这样的服务,但同样,Firebase 有自己的 SDK,因此可以通过某种方式绕过。

来到涉及在后端验证 reCaptcha 令牌的 reCaptcha 案例,您可能会在一定程度上摆脱机器人。但是,如果有人在没有有效令牌的情况下继续向您的服务器发送垃圾邮件,您的函数仍然会向您收取验证令牌所花费的时间。

如果您还有其他问题,请告诉我。

【讨论】:

以上是关于保护 Firebase Cloud Function HTTP 端点的主要内容,如果未能解决你的问题,请参考以下文章

Cloud Firestore 安全规则只允许从 Firebase 函数写入

使用 Google Cloud KMS 进行 Firebase 实时数据库加密的最佳实践 [关闭]

如何从Swift中的Cloud FireStore Firebase访问特定字段

Cloud Functions 部署失败:“加载用户代码时函数失败。错误消息:无法加载文件 lib/index.js 中的代码。”

如何在 Firebase Cloud 函数中调用导出的 https Firebase Cloud 函数?

Firebase Cloud Functions 访问 Firebase 托管文件