PAYATHON 2026

支付网关回调触发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 状态码必须符合支付网关的回调协议,否则网关可能持续重试。

备注:内容仅供参考。