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

问题描述

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

该表单应该对浏览器和curl / postman类型的请求都是安全的。

环境

后端-Node.js,MongoDB Atlas和Azure Web应用。
前端-jQuery。

下面是我目前正在执行的实施的详细概述,但希望不会太笼统。

以下是我对实施的疑问。

使用的相关库

Helmet-通过设置各种HTTP标头(包括Content Security Policy
)来帮助保护Express应用程序 reCaptcha v3-防止垃圾邮件和其他类型的自动滥用
DOMPurify-XSS消毒剂
validator.js-字符串验证程序和消毒程序库
he-HTML实体编码器/解码器

一般数据流为:

/*
on click event:  
- get sanitized data
- perform some validations
- html encode the values
- get recaptcha v3 token from google
- send all data,including token,to server
- send token to google to verify
- if the response 'score' is above 0.5,add the submission to the database  
- return the entry to the client and populate the DOM with the submission   
*/ 

POST请求-浏览器

// test input:  
// <script>alert("hi!")</script><h1>hello there!</h1> <a href="">link</a>

// sanitize the input  
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 });

// validation - make sure input is between 1 and 140 characters
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,max: 140 });

// if validations pass
if (input_1_text_valid_length === true && input_2_text_valid_length === true) {

/* 
encode the sanitized input 
not sure if i should encode BEFORE adding to MongoDB  
or just add to database "as is" and encode BEFORE displaying in the DOM with $("#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);

// define parameters to send to database  
var parameters = {};
parameters.input_1_text = sanitized_encoded_input_1_text; 
parameters.input_2_text = sanitized_encoded_input_2_text; 

// get token from google and send token and input to database
// see:  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}`;

// verify recaptcha token with google
var response = await fetch(url);
var response_json = await response.json();
var score = response_json.score;
var document = {};

/*
if google's response 'score' is greater than 0.5,add submission to the database and populate client DOM with $("#output").prepend(html); 
see: https://developers.google.com/recaptcha/docs/v3#interpreting_the_score
*/
if (score >= 0.5) {

    // add submission to database 
    // return submisson to client to update the DOM
    // DOM will just display this text:  <h1>hello there!</h1> <a href="">link</a>
}); 

获取页面加载请求

逻辑/假设:

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

curl,邮递员等的POST请求

逻辑/假设:

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

服务器上的头盔配置

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","data:"],frameSrc: ["'self'","https://www.google.com"]
            }
        },})
);

问题

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

  2. 如果将值 保存为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;

  3. 在我阅读的有关保护Web表单安全的内容中,有很多关于客户端实践的说法,因为可以在DOM中进行任何更改,可以禁用JavaScript以及可以使用以下方法直接向API端点发出请求:卷曲或邮递员,因此绕过任何客户端方法。

  4. 这样说,应该执行清理(DOMPurify),验证(validator.js)和编码(he):1)仅客户端2)客户端和服务器端还是3)仅服务器端?

为彻底起见,这是另一个相关问题:

从客户端向服务器发送数据时,以下任何组件是否进行任何自动转义或HTML编码?我问,因为如果这样做,可能会导致不必要的手动转义或编码。

  • jQuery ajax()请求
  • Node.js
  • 快递
  • 头盔
  • bodyParser(节点程序包)
  • MongoDB本机驱动程序
  • MongoDB

解决方法

您应该始终不确定在使用前在后端清理您使用的每个数据!

请参见https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html

,

在阅读了有关该主题的更多信息之后,这就是我想出的方法:

点击事件:

  • 清理数据(DOMPurify
  • 验证数据(validator.js
  • 从Google(reCaptcha v3)获取Recaptcha v3令牌
  • 将所有数据(包括令牌)发送到服务器
  • 服务器正在使用Helmet
  • 服务器使用Express Rate LimitRate Limit Mongo将每POST毫秒(按IP地址)在某条路由上的X请求限制为X
  • 服务器位于Cloudflare代理后面,该代理提供了一些安全性和缓存功能(需要在节点服务器文件中设置app.set('trust proxy',true),以便速率限制器能够提取用户的实际IP地址-请参阅{{3} })
  • 从服务器向Google发送令牌以进行验证(Express behind proxies
  • 如果响应的“得分”高于0.5,请再次执行相同的排序和验证
  • 如果验证通过,则使用moderated标志值false将标签添加到数据库中

我决定立即进行手动审核,而不是立即将条目返回浏览器,而是将条目的moderated值更改为true。尽管它消除了用户对响应的直接要求,但如果不立即发布响应,它也减少了垃圾邮件发送者的诱惑。

  • 页面加载时的GET请求然后返回所有moderated: true的条目
  • HTML在显示值之前对其进行编码(reCaptcha v3
  • 使用HTML编码的条目填充DOM

代码看起来像这样:

POST请求-浏览器

// sanitize the input  
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 });

// validation - make sure input is between 1 and 140 characters
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,max: 140 });

// validation - regex to only allow certain characters
// for pattern,see:  https://stackoverflow.com/q/63895992
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,"gm");

// if validations pass
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) {

// define parameters to send to database  
var parameters = {};
parameters.input_1_text = sanitized_input_1_text; 
parameters.input_2_text = sanitized_input_2_text; 

// get token from google and send token and input to database
// see:  https://developers.google.com/recaptcha/docs/v3#programmatically_invoke_the_challenge
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}`;

// verify recaptcha token with google
var response = await fetch(url);
var response_json = await response.json();
var score = response_json.score;
var document = {};

// if google's response 'score' is greater than 0.5,// see: https://developers.google.com/recaptcha/docs/v3#interpreting_the_score  

if (score >= 0.5) {

// perform all the same sanitizations and validations to protect against
// POST requests direct to the API via curl or postman etc  
// if validations pass,add entry to the database with `moderated: false` property   


}); 

获取请求-浏览器

逻辑:

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

服务器上的头盔配置

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","data:","https://another-domain.com"],frameSrc: ["'self'","https://www.google.com"]
            }
        },})
);

在OP中回答我的问题:

  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)和编码(他)可以执行以下任一操作:1)客户端 仅2)客户端和服务器端还是3)仅服务器端?

选项2,清理并验证客户端服务器上的输入。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...