ValidateRequest

ASP.NET 1.1引入了自动检查XSS的功能,通过引发HttpRequestValidationException阻止潜在危险请求。文章探讨了正确处理该异常的方法,包括使用Page_Error函数自定义错误提示而非简单禁用防护。
ASP.Net 1.1后引入了对提交表单自动检查是否存在XSS(跨站脚本攻击)的能力。当用户试图用之类的输入影响页面返回结果的时候,ASP.Net的引擎会引发一 个 HttpRequestValidationExceptioin。默认情况下会返回如下文字的页面:

 

  这是ASP.Net提供的一个很重要的安全特性。因为很多程序员对安全没有概念,甚至都不知道XSS这种攻击的存在,知道主动去防护的就更少了。ASP.Net在这一点上做到默认安全。这样让对安全不是很了解的程序员依旧可以写出有一定安全防护能力的网站。

  但是,当我Google搜索 HttpRequestValidationException 或者 "A potentially dangerous Request.Form value was detected from the client"的时候,惊奇的发现大部分人给出的解决方案竟然是在ASP.Net页面描述中通过设置 validateRequest=false 来禁用这个特性,而不去关心那个程序员的网站是否真的不需要这个特性。看得我这叫一个胆战心惊。安全意识应该时时刻刻在每一个程序员的心里,不管你对安全的概念了解多少,一个主动的意识在脑子里,你的站点就会安全很多。

  为什么很多程序员想要禁止 validateRequest 呢?有一部分是真的需要用户输入"<>"之类的字符。这就不必说了。还有一部分其实并不是用户允许输入那些容易引起XSS的字符,而是讨厌这 种报错的形式,毕竟一大段英文加上一个ASP.Net典型异常错误信息,显得这个站点出错了,而不是用户输入了非法的字符,可是自己又不知道怎么不让它报 错,自己来处理报错。

  对于希望很好的处理这个错误信息,而不使用默认ASP.Net异常报错信息的程序员们,你们不要禁用validateRequest=false。

  正确的做法是在你当前页面添加Page_Error()函数,来捕获所有页面处理过程中发生的而没有处理的异常。然后给用户一个合法的报错信 息。如果当前页面没有Page_Error(),这个异常将会送到Global.asax的Application_Error()来处理,你也可以在那 里写通用的异常报错处理函数。如果两个地方都没有写异常处理函数,才会显示这个默认的报错页面呢。

举例而言,处理这个异常其实只需要很简短的一小段代码就够了。在页面的Code-behind页面中加入这么一段代码:

 

 

  这样这个程序就可以截获 HttpRequestValidationException 异常,而且可以按照程序员的意愿返回一个合理的报错信息。

  这段代码很简单,所以我希望所有不是真的要允许用户输入之类字符的朋友,千万不要随意的禁止这个安全特性,如果只是需要异常处理,那么请用类似于上面的代码来处理即可。

  而对于那些通过 明确禁止了这个特性的程序员,自己一定要明白自己在做什么,而且一定要自己手动的检查必须过滤的字符串,否则你的站点很容易引发跨站脚本攻击。

  关于存在Rich Text Editor的页面应该如何处理?

  如果页面有富文本编辑器的控件的,那么必然会导致有类的HTML标签提交回来。在这种情况下,我们不得不将validateRequest="false"。那么安全性怎么处理?如何在这种情况下最大限度的预防跨站脚本攻击呢?

  根据微软的建议,我们应该采取安全上称为“默认禁止,显式允许”的策略。

  首先,我们将输入字符串用 HttpUtility.HtmlEncode()来编码,将其中的HTML标签彻底禁止。

  然后,我们再对我们所感兴趣的、并且是安全标签,通过Replace()进行替换。比如,我们希望有""标签,那么我们就将""显式的替换回""。

  示例代码如下:

 

这样我们即允许了部分HTML标签,又禁止了危险的标签。

 

  根据微软提供的建议,我们要慎重允许下列HTML标签,因为这些HTML标签都是有可能导致跨站脚本攻击的。

以下是引用片段:
void submitBtn_Click(object sender, EventArgs e)
...{
// 将输入字符串编码,这样所有的HTML标签都失效了。
StringBuilder sb = new StringBuilder(
HttpUtility.HtmlEncode(htmlInputTxt.Text));
// 然后我们选择性的允许<b> 和 <i>
sb.Replace("&lt;b&gt;", "<b>");
sb.Replace("&lt;/b&gt;", "");
sb.Replace("&lt;i&gt;", "<i>");
sb.Replace("&lt;/i&gt;", "");
Response.Write(sb.ToString());
}
以下是引用片段:
protected void Page_Error(object
sender, EventArgs e)
{
Exception ex
=
Server.GetLastError();
if (ex is
HttpRequestValidationException)
{
Response.Write(
"请您输入合法字符串。"
);
Server.ClearError();
// 如果不ClearError()这个异常会继续传到Application_Error()。

}
}
以下是引用片段:
Server Error in ''/YourApplicationPath'' Application

A potentially dangerous Request.Form value was detected from the client
(txtName="<b>").

Description: Request Validation has detected a potentially dangerous client input value, and processing of the request has been aborted. This value may indicate an attempt to compromise the security of your application, such as a cross-site scripting attack. You can disable request validation by setting validateRequest=false in the Page directive or in the configuration section. However, it is strongly recommended that your application explicitly check all inputs in this case.

Exception Details: System.Web.HttpRequestValidationException: A potentially dangerous Request.Form value was detected from the client (txtName="<b>").

....
/** * 基础云和 Vms之间的http传输的http client代理 * * @author WangYongJian * @version 1.0 * @since 2023/2/3 16:47 */ @Slf4j @Component public class HttpClientProxy { /** * 云对外设备接口client,注入CloudSDK */ private ApiHttpClient apiHttpClient; /** * 云对外APP接口client,注入CloudSDK */ private AppHttpClient appHttpClient; /** * 云端控制器信息存储服务,在本地存储控制器信息,构建了数据库 */ private CloudInfoRepository cloudInfoRepository; private static final String KEY_TRUST_STORE_PATH = "javax.net.ssl.trustStore"; private static final String KEY_TRUST_STORE_PASS = "javax.net.ssl.trustStorePassword"; /** * 构造器,同时构造ApiHttpClient和AppHttpClient http客户端代理 * * @param cloudInfoRepository 云设备信息响应服务 * */ @Autowired private HttpClientProxy(CloudInfoRepository cloudInfoRepository, CloudOwnerCache cloudOwnerCache, VmsInternalApiService settingsService) { String vmsHome = AppHomeUtils.getConfPath(); String keyStorePath = vmsHome + "tplinknbu.jks"; System.setProperty(CLOUDACCESS_SEF_ENTRY,CLOUDACCESS_SEF_ENTRY_URL); System.setProperty(CLOUDACCESS_DEVICE_ENTRY,CLOUDACCESS_DEVICE_ENTRY_URL); System.setProperty(CLOUDACCESS_WAP_ENTRY,CLOUDACCESS_WAP_ENTRY_URL); System.setProperty(CLOUDACCESS_API_ENTRY,CLOUDACCESS_API_ENTRY_URL); System.setProperty(KEY_TRUST_STORE_PATH, keyStorePath); System.setProperty(KEY_TRUST_STORE_PASS, "nettyNbu"); IDeviceMgmt deviceMgmt = new VmsDevice(cloudInfoRepository, cloudOwnerCache, settingsService); this.cloudInfoRepository = cloudInfoRepository; /* * 'validate' request to API server must use returned validateHost's value of 'applySoftwareDeviceId' request * before first binding account successfully, then 'validate' request can use default host once bound * successfully . */ if (this.cloudInfoRepository.checkDeviceIdIsRaw()) { // rawId is true, current deviceId has never bound to a user account // 从本地数据库中 获取 validateHost String validateHost = this.cloudInfoRepository.getValidateHostBeforeBind(); if (Objects.isNull(validateHost)) { apiHttpClient = new ApiHttpClient(deviceMgmt); } else { // 使用 validateHost 初始化创建一个 apiHttpClient apiHttpClient = new ApiHttpClient(validateHost, deviceMgmt); } } else { apiHttpClient = new ApiHttpClient(deviceMgmt); } appHttpClient = new AppHttpClient(deviceMgmt); } /** * Send post request to API cloud server. <br> It uses default connection timeout(15s) and read timeout(15s) for per * request. If current deviceToken is expired, validate request will be sent automatically to get new deviceToken; * * @param leafPath points out the method name of URL. * @param request API request. * @return response from API server. */ @SneakyThrows public JSONObject sendPostRequest(String leafPath, JSONObject request) { return apiHttpClient.sendPostRequest(leafPath, request); } /** * Send post request to API cloud server. <br> It uses default connection timeout(15s) and read timeout(15s). * * @param leafPath points out the method name of URL. * @param deviceToken device token from validateHost. * @param request API request. * @return response from API server. */ @SneakyThrows public JSONObject sendPostRequest(String leafPath, String deviceToken, JSONObject request) { return apiHttpClient.sendPostRequest(leafPath, deviceToken, request); } /** * Send post request to API cloud server. <br> * * @param leafPath points out the method name of URL. * @param deviceToken device token from validateHost. * @param request API request. * @param connTimeout connection timeout. Valid range is 1-30. * @param readTimeout read timeout. Valid range is 1-30. * @return response from API server. */ @SneakyThrows public JSONObject sendPostRequest(String leafPath, String deviceToken, JSONObject request, int connTimeout, int readTimeout) { return apiHttpClient.sendPostRequest(leafPath, deviceToken, request, connTimeout, readTimeout); } /** * Restores host to validate device legality for 'validate' request to the default value. <br> * 将主机还原为默认值,以验证“validate”请求的设备合法性。 */ public void restoreValidateHost() { apiHttpClient.restoreValidateHost(); } /** * 向基础云申请VMS Local的软件ID * <p> * Apply device id to APP cloud server and updates member 'validateHost' of member 'apiHttpClient', this method can * only be called by software EAP Controller.<br> * 将设备ID申请到APP云服务器,并更新成员变量'apiHttpClient'的成员变量'validateHost',该方法只能由软件EAP控制器调用。 This function can login automatically * to get user token. * <B>Note:</B> 'validate' request to API server must use returned validateHost's value of this request before * first binding account successfully, then 'validate' request can use default host once bound successfully.<br> * * @param urlParams parameters included in URL; the key stores the parameter name, the value stores the * parameter value. * @param cloudUserName cloud account that applies device id. * @param cloudPassword password of cloud account. * @return response from APP cloud server. */ @SneakyThrows public JSONObject applySoftwareDeviceId(Map<String, String> urlParams, String cloudUserName, String cloudPassword) { if (CollectionUtil.isEmpty(urlParams) || StringUtil.isBlank(cloudUserName) || StringUtil.isBlank( cloudPassword)) { log.error("[applySoftwareDeviceId] param invalid cloudUsername {}", CustomPIIMaskUtil.hash(cloudUserName)); return null; } JSONObject response = GrpcRetryUtil.executeWithRetry(()->appHttpClient.applySoftwareDeviceId(urlParams, cloudUserName,cloudPassword)); /* * parse validateHost and update API validateHost. * 解析 * */ if ((response != null) && CloudUtils.hasKey(response, AppHttpClient.KEY_ERROR_CODE) && response.getInt (AppHttpClient.KEY_ERROR_CODE) == 0) { if (CloudUtils.hasKey(response, AppHttpClient.KEY_RESULT)) { JSONObject result = response.getJSONObject(AppHttpClient.KEY_RESULT); // 从result中获取新的 DeviceId // 从result中获取新的 validateHost String newDeviceId = result.getString(CloudConstant.DEVICE_ID); String validateHost = result.getString(AppHttpClient.KEY_VALIDATE_HOST); if (!Objects.isNull(validateHost)) { // 给cloud sdk 设置 validateHost apiHttpClient.setValidateHost(validateHost); // 给 本地数据库 设置 validateHost cloudInfoRepository.setValidateHostBeforeBind(validateHost); } else { log.error("Value of {} is not valid.", AppHttpClient.KEY_VALIDATE_HOST); } cloudInfoRepository.saveDeviceId(newDeviceId); } } return response; } }
最新发布
09-17
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值