支付网关回调触发Potentially dangerous Request.Form错误
结论
支付网关把 XML 放在表单字段中时,不要使用 Request.Form.Get("NotifyData") 读取,因为这会触发 ASP.NET 请求验证。对于 ASP.NET MVC 4 / .NET Framework 4.5 及以上版本,应明确读取未经验证的表单值:
string notifyMessage = Request.Unvalidated.Form["NotifyData"];
[ValidateInput(false)] 只会关闭 MVC 层的自动输入验证。在某些框架版本、请求验证模式或模型绑定流程中,Request.Form 仍可能提前触发 ASP.NET 请求验证。使用 Request.Unvalidated 读取指定字段更直接,也不必关闭整个请求的验证。
为什么 XML 会被判定为危险输入
ASP.NET 会检查表单、查询字符串等请求输入。如果字段值包含以下内容:
<?xml version="1.0" encoding="utf-8"?>
<Notify xmlns="...">
...
</Notify>
其中的 <、>、XML 声明和标签可能被识别为潜在的 HTML 或脚本输入,从而抛出异常:
A potentially dangerous Request.Form value was detected
这不代表 XML 本身有问题。ASP.NET 只是无法判断这些标记之后会被如何使用。
Action 中的以下参数还会在方法执行前参与模型绑定:
FormCollection form
模型绑定器可能提前读取表单集合,导致验证在方法体读取字段前就已发生。因此,即使 Action 添加了 [ValidateInput(false)],在不同的 ASP.NET MVC 和 .NET Framework 配置下仍可能出现异常。
推荐实现
移除不需要的 FormCollection 参数,仅对包含 XML 的字段进行未验证读取:
using System.Web.Mvc;
public class PaymentController : Controller
{
[HttpPost]
public ActionResult PDTHandler()
{
string notifyMessage = Request.Unvalidated.Form["NotifyData"];
if (string.IsNullOrWhiteSpace(notifyMessage))
{
return new HttpStatusCodeResult(400, "Missing NotifyData");
}
// 先验证支付网关签名,再解析和处理 XML。
// VerifyGatewaySignature(Request, notifyMessage);
// ParseNotifyData(notifyMessage);
return Content("OK");
}
}
这里的“未验证”只表示跳过 ASP.NET 针对 HTML 和脚本的通用检查,并不代表数据可以直接信任。支付回调仍须进行签名校验、金额核对、订单状态检查和安全的 XML 解析。
使用强类型模型接收
如果回调字段的结构固定,也可以使用强类型模型,并只允许 NotifyData 包含标记字符:
using System.Web.Mvc;
public class PaymentCallbackModel
{
[AllowHtml]
public string NotifyData { get; set; }
public string Signature { get; set; }
}
Action:
[HttpPost]
public ActionResult PDTHandler(PaymentCallbackModel model)
{
if (string.IsNullOrWhiteSpace(model.NotifyData))
{
return new HttpStatusCodeResult(400, "Missing NotifyData");
}
// VerifyGatewaySignature(model.Signature, model.NotifyData);
// ParseNotifyData(model.NotifyData);
return Content("OK");
}
[AllowHtml] 这个名称容易让人误解。它不是鼓励接收 HTML,而是允许该属性包含通常会被请求验证拦截的标记字符。该属性用于 MVC 模型绑定。如果代码直接读取 Request.Form,仍应改用 Request.Unvalidated.Form。
安全解析 XML
回调数据来自外部系统。解析时应禁用 DTD 和外部实体,防止 XXE 等问题:
using System.IO;
using System.Xml;
using System.Xml.Linq;
private static XDocument ParseNotifyData(string xml)
{
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersInDocument = 1024 * 1024
};
using (var stringReader = new StringReader(xml))
using (var xmlReader = XmlReader.Create(stringReader, settings))
{
return XDocument.Load(xmlReader, LoadOptions.None);
}
}
大小限制需要根据支付网关的实际协议调整,不能假定所有回调都小于示例中的 1 MB。
旧版框架的处理
如果项目使用的版本没有 Request.Unvalidated,可以继续使用 MVC 的 [ValidateInput(false)]:
[HttpPost]
[ValidateInput(false)]
public ActionResult PDTHandler()
{
string notifyMessage = Request.Form["NotifyData"];
return Content("OK");
}
如果请求验证仍在 Action 执行前触发,应检查项目的 .NET Framework 和 ASP.NET MVC 版本,以及 web.config 中的 httpRuntime requestValidationMode。旧项目有时会通过兼容模式延迟验证:
<configuration>
<system.web>
<httpRuntime requestValidationMode="2.0" />
</system.web>
</configuration>
这项配置会影响整个应用程序,不适合作为首选方案。修改前必须确认项目版本,并检查其他页面是否依赖请求验证。如果项目可以升级并使用 Request.Unvalidated.Form["NotifyData"],应优先采用字段级处理。
处理支付回调时还要注意
- 必须按照支付网关协议验证签名或消息摘要,不能只根据 XML 中的“支付成功”字段更新订单。
- 签名通常需要使用回调中的原始字符串。验签前不要格式化 XML、修改换行、改变编码或重新序列化。
- 核对订单号、商户号、币种、金额和当前订单状态。
- 回调处理应具备幂等性,因为支付网关可能重复发送通知。
- 不要将未经编码的回调内容直接输出到 HTML 页面,否则仍可能引发 XSS。
- 限制请求体大小、XML 大小和处理时间。
- 只有网关协议明确提供可靠且稳定的来源地址时,才能把 IP 白名单作为辅助校验。IP 白名单不能替代签名验证。
- 返回内容和 HTTP 状态码必须符合支付网关的回调协议,否则网关可能持续重试。
备注:内容仅供参考。
