当保护评论表单和相关的API端点时,输入应该在浏览器、服务器或两者中进行消毒、验证和编码?

9 浏览
0 Comments

当保护评论表单和相关的API端点时,输入应该在浏览器、服务器或两者中进行消毒、验证和编码?

我正在尝试在非CMS环境中尽可能安全地保护一个评论表单,该环境没有用户身份验证。

该表单应该能够防止浏览器和curl/postman类型的请求攻击。

环境

后端 - Node.js、MongoDB Atlas和Azure Web应用。

前端 - jQuery。

以下是我目前工作实现的详细概述,希望不会太过复杂。

接下来是我对该实现的问题。

使用的相关库

Helmet - 通过设置各种HTTP头部信息(包括内容安全策略)来帮助保护Express应用程序。

reCaptcha v3 - 用于防止垃圾邮件和其他类型的自动滥用

DOMPurify - 一个XSS(跨站脚本)清理工具

validator.js - 一个字符串验证和清理工具库

he - 一个HTML实体编码/解码器

数据的一般流程如下:

/*
点击事件:
- 获取经过清理的数据
- 进行一些验证
- 对值进行HTML编码
- 从Google获取recaptcha v3令牌
- 将所有数据(包括令牌)发送到服务器
- 发送令牌到Google进行验证
- 如果响应的'score'大于0.5,则将提交添加到数据库中
- 将条目返回给客户端,并用提交填充DOM
*/ 

浏览器中的POST请求

// 测试输入:
// 

hello there!

link // 对输入进行清理 var sanitized_input_1_text = DOMPurify.sanitize($input_1.val().trim(), { SAFE_FOR_JQUERY: true }); var sanitized_input_2_text = DOMPurify.sanitize($input_2.val().trim(), { SAFE_FOR_JQUERY: true }); // 验证 - 确保输入在1到140个字符之间 var input_1_text_valid_length = validator.isLength(sanitized_input_1_text, { min: 1, max: 140 }); var input_2_text_valid_length = validator.isLength(sanitized_input_2_text, { min: 1, max: 140 }); // 如果验证通过 if (input_1_text_valid_length === true && input_2_text_valid_length === true) { /* 对清理后的输入进行编码 不确定是否应在添加到MongoDB之前进行编码 还是在显示在DOM中之前对其进行编码(使用$("#ouput").html(html_content)) */ var sanitized_encoded_input_1_text = he.encode(input_1_text); var sanitized_encoded_input_2_text = he.encode(input_2_text); // 定义要发送到数据库的参数 var parameters = {}; parameters.input_1_text = sanitized_encoded_input_1_text; parameters.input_2_text = sanitized_encoded_input_2_text; // 从Google获取令牌并将令牌和输入发送到数据库 // 参考:https://developers.google.com/recaptcha/docs/v3#programmatically_invoke_the_challenge grecaptcha.ready(function() { grecaptcha.execute('site-key-here', { action: 'submit' }).then(function(token) { parameters.token = token; jquery_ajax_call_to_my_api(parameters); }); }); }

服务器中的POST请求

var secret_key = process.env.RECAPTCHA_SECRET_SITE_KEY;
var token = req.body.token;
var url = `https://www.google.com/recaptcha/api/siteverify?secret=${secret_key}&response=${token}`;
// 与Google验证recaptcha令牌
var response = await fetch(url);
var response_json = await response.json();
var score = response_json.score;
var document = {};
/*
如果Google的响应'score'大于0.5,
将提交添加到数据库,并用$("#output").prepend(html)填充客户端的DOM
参考:https://developers.google.com/recaptcha/docs/v3#interpreting_the_score
*/
if (score >= 0.5) {
    // 将提交添加到数据库
    // 将提交返回给客户端以更新DOM
    // DOM将只显示此文本:

hello there!

link });

页面加载时的GET请求

逻辑/假设:

  • 获取所有提交内容,返回给客户端并用$("#output").html(html_content)添加到DOM中。
  • 不需要在填充DOM之前对值进行编码,因为值已经在数据库中进行了编码?

来自curl、postman等的POST请求

逻辑/假设:

  • 它们没有Google令牌,因此无法从服务器验证令牌,也不能向数据库添加条目?

服务器上的Helmet配置

app.use(
    helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'", "https://somedomain.io", "https://maps.googleapis.com", "https://www.google.com", "https://www.gstatic.com"],
                styleSrc: ["'self'", "fonts.googleapis.com", "'unsafe-inline'"],
                fontSrc: ["'self'", "fonts.gstatic.com"],
                imgSrc: ["'self'", "https://maps.gstatic.com", "https://maps.googleapis.com", "data:"],
                frameSrc: ["'self'", "https://www.google.com"]
            }
        },
    })
);

问题

  1. 我应该将值作为HTML编码实体添加到MongoDB数据库中,还是将它们"原样"存储,然后在用它们填充DOM之前对它们进行编码?

  2. 如果值以HTML实体形式保存在MongoDB中,这是否会使得在数据库中搜索内容变得困难?例如,搜索"

    hello there!

    link将不会返回任何结果,因为数据库中的值是<h1>hello there!</h1> <a href="">link</a>

  3. 在我阅读关于保护Web表单的文章中,提到客户端端的做法在很大程度上是多余的,因为DOM中的任何内容都可以更改,JavaScript可以被禁用,并且可以直接使用curl或postman进行API端点请求,从而绕过任何客户端端的方法。

  4. 在这种情况下,清理(DOMPurify)、验证(validator.js)和编码(he)应该是在客户端端执行、客户端端和服务器端都执行,还是仅在服务器端执行?

为了全面,这里还有另一个相关问题:

以下组件在从客户端发送数据到服务器时是否会自动进行转义或HTML编码?我问这个问题是因为如果它们这样做了,那么手动进行转义或编码可能就不必要了。

  • jQuery的ajax()请求
  • Node.js
  • Express
  • Helmet
  • bodyParser(Node.js包)
  • MongoDB原生驱动程序
  • MongoDB
0
0 Comments

当保护评论表单和相关的API端点时,输入数据应该在浏览器、服务器还是两者都进行清理、验证和编码?

在阅读了更多相关主题后,我想出了以下方法:

点击事件:

  • 清理数据(DOMPurify)
  • 验证数据(validator.js)
  • 从Google获取reCaptcha v3令牌
  • 将包括令牌在内的所有数据发送到服务器
  • 服务器使用Helmet插件
  • 服务器使用Express Rate Limit和Rate Limit Mongo来限制某个路由上的POST请求(按IP地址)
  • 服务器在Cloudflare代理后面,提供一些安全和缓存功能(需要在node服务器文件中设置app.set('trust proxy', true)以便速率限制器能够获取用户的实际IP地址)
  • 从服务器发送令牌给Google进行验证
  • 如果响应的“score”大于0.5,则再次进行清理和验证
  • 如果验证通过,将带有“moderated”标志值为false的条目添加到数据库中

我决定不立即将条目返回到浏览器,而是要求进行手动审核的过程,这涉及将条目的“moderated”值更改为true。虽然这会减缓用户的响应速度,但如果响应不会立即发布,对于垃圾邮件等来说就不那么诱人。

  • 页面加载时的GET请求会返回所有“moderated: true”的条目
  • 在显示之前对值进行HTML编码(he)
  • 用HTML编码的条目填充DOM

代码如下:

浏览器端的POST请求

// 清理输入数据
var sanitized_input_1_text = DOMPurify.sanitize($input_1.val().trim(), { SAFE_FOR_JQUERY: true });
var sanitized_input_2_text = DOMPurify.sanitize($input_2.val().trim(), { SAFE_FOR_JQUERY: true });
// 验证输入数据长度在1到140个字符之间
var input_1_text_valid_length = validator.isLength(sanitized_input_1_text, { min: 1, max: 140 });
var input_2_text_valid_length = validator.isLength(sanitized_input_2_text, { min: 1, max: 140 });
// 使用正则表达式验证输入数据只包含某些字符
var pattern = /^(?!.*([ ,'-])\1)[a-zA-Z]+(?:[ ,'-]+[a-zA-Z]+)*$/;
var input_1_text_valid_characters = validator.matches(sanitized_input_1_text, pattern, "gm");
var input_2_text_valid_characters = validator.matches(sanitized_input_2_text, pattern, "gm");
// 如果验证通过
if (input_1_text_valid_length === true && input_2_text_valid_length === true && input_1_text_valid_characters === true && input_2_text_valid_characters === true) {
    // 定义要发送到数据库的参数
    var parameters = {};
    parameters.input_1_text = sanitized_input_1_text; 
    parameters.input_2_text = sanitized_input_2_text; 
    // 从Google获取令牌并将令牌和输入数据发送到数据库
    grecaptcha.ready(function() {
        grecaptcha.execute('site-key-here', { action: 'submit_entry' }).then(function(token) {
            parameters.token = token;
            jquery_ajax_call_to_my_api(parameters);
        });
    });
}

服务器端的POST请求

var secret_key = process.env.RECAPTCHA_SECRET_SITE_KEY;
var token = req.body.token;
var url = `https://www.google.com/recaptcha/api/siteverify?secret=${secret_key}&response=${token}`;
// 用Google验证reCaptcha令牌
var response = await fetch(url);
var response_json = await response.json();
var score = response_json.score;
var document = {};
// 如果Google的响应“score”大于等于0.5
if (score >= 0.5) {
    // 执行相同的清理和验证,以保护API免受直接发送到API的POST请求(如通过curl或postman)
    // 如果验证通过,则将条目添加到数据库中,设置“moderated: false”
}

浏览器端的GET请求

逻辑:

  • 获取所有“moderated: true”的条目
  • 在填充DOM之前对值进行HTML编码

服务器端的Helmet配置

app.use(
    helmet({
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'", "https://maps.googleapis.com", "https://www.google.com", "https://www.gstatic.com"],
                connectSrc: ["'self'", "https://some-domain.com", "https://some.other.domain.com"],
                styleSrc: ["'self'", "fonts.googleapis.com", "'unsafe-inline'"],
                fontSrc: ["'self'", "fonts.gstatic.com"],
                imgSrc: ["'self'", "https://maps.gstatic.com", "https://maps.googleapis.com", "data:", "https://another-domain.com"],
                frameSrc: ["'self'", "https://www.google.com"]
            }
        },
    })
);

回答了我在问题中的问题:

  1. 我应该将值作为HTML编码实体保存在MongoDB中,还是将它们存储“原样”,只在填充DOM之前对它们进行编码?

只要在客户端和服务器上对输入进行了清理和验证,你只需要在填充DOM之前进行HTML编码。

  1. 如果值在MongoDB中保存为HTML实体,那么在数据库中搜索内容会变得困难,因为搜索例如<h1>hello there!</h1> <a href="">link</a> 的结果将为空,因为数据库中的值是 &#x3C;h1&#x3E;hello there!&#x3C;/h1&#x3E; &#x3C;a href=&#x22;&#x22;&#x3E;link&#x3C;/a&#x3E;

我觉得如果数据库条目充斥着HTML编码的值,会使数据库条目看起来很乱,所以我将经过清理和验证的条目保存为“原样”。

  1. 在我阅读关于保护Web表单的文章中,有很多关于客户端方法相当多余的说法,因为DOM中的任何内容都可以更改,JavaScript可以被禁用,并且可以直接使用curl或postman等工具向API端点发送请求,从而绕过任何客户端方法。
  2. 在这种情况下,清理(DOMPurify)、验证(validator.js)和编码(he)应该是在客户端、服务器端还是两者都要进行?

选项2,对客户端和服务器端的输入进行清理和验证。

0
0 Comments

在保护评论表单和相关API端点时,应该在浏览器、服务器或两者都进行输入的清理、验证和编码的操作?

问题的出现原因:

为了确保网站和应用程序的安全性,对于用户输入的数据,我们需要进行清理、验证和编码的操作。这是因为用户输入的数据可能包含恶意代码或脚本,如果不进行处理,就有可能导致安全漏洞,比如跨站脚本攻击(XSS)。

解决方法:

为了解决这个问题,我们可以在浏览器和服务器两个环节都对用户输入的数据进行处理。

在浏览器端,我们可以使用JavaScript代码对用户输入进行清理、验证和编码。例如,可以使用正则表达式来校验输入的格式是否符合要求,使用encodeURIComponent()函数来对特殊字符进行编码,以防止XSS攻击。

示例代码:

const userInput = document.getElementById('commentInput').value;
const sanitizedInput = userInput.replace(/