PAYATHON 2026

支付后如何安全定义 redirect_to URL?

支付阿杰

结论

不要把 $_GET['redirect_to'] 直接拼接到 Location 响应头中。hidden input 的值完全由用户控制,这样做会造成开放重定向(Open Redirect)漏洞。

重定向时,服务器通常不会主动请求目标网站,而是通过 Location 响应头通知用户的浏览器访问该地址。但风险并不会因此消失。攻击者仍可利用你的可信域名,将用户引向钓鱼网站、恶意下载页面或伪造的支付结果页。

更安全的方式是只允许跳转到事先批准的站内路径。更进一步,可以不接收 URL,只接收由服务器解析的目标标识。

推荐方案:只传递目标标识

如果支付完成后只有几个固定去向,就不必让客户端提交完整 URL。客户端只需提交一个简单标识,实际跳转地址由服务器决定。

<input type="hidden" name="redirect_to" value="orders">
<?php

$redirectMap = [
    'orders'  => '/account/orders',
    'profile' => '/account/profile',
    'home'    => '/',
];

$key = $_GET['redirect_to'] ?? '';
$target = $redirectMap[$key] ?? '/payment/result';

header('Location: ' . $target, true, 303);
exit;

这段代码不信任用户提交的地址。即使用户将标识改成其他内容,也只会被重定向到默认页面。

支付完成后通常适合使用 303 See Other,让浏览器通过 GET 请求结果页,避免用户刷新页面时再次提交与支付有关的 POST 请求。具体使用哪个状态码,仍要根据支付流程来确定。

需要动态地址时,只允许站内路径

如果业务确实需要动态跳转,可以只接受以单个 / 开头的站内绝对路径,并拒绝协议相对地址、反斜杠和控制字符等危险输入。

<?php

function getSafeRedirectPath(mixed $value, string $fallback): string
{
    if (!is_string($value) || $value === '') {
        return $fallback;
    }

    // 拒绝换行符和其他 ASCII 控制字符
    if (preg_match('/[\x00-\x1F\x7F]/', $value)) {
        return $fallback;
    }

    // 必须以单个正斜杠开头
    // 拒绝 //evil.example 和反斜杠形式
    if ($value[0] !== '/' || str_starts_with($value, '//')) {
        return $fallback;
    }

    if (str_contains($value, '\\')) {
        return $fallback;
    }

    $parts = parse_url($value);

    if ($parts === false) {
        return $fallback;
    }

    // 站内相对目标不应包含协议、主机、用户信息或端口
    foreach (['scheme', 'host', 'user', 'pass', 'port'] as $component) {
        if (array_key_exists($component, $parts)) {
            return $fallback;
        }
    }

    return $value;
}

$target = getSafeRedirectPath(
    $_GET['redirect_to'] ?? null,
    '/payment/result'
);

header('Location: ' . $target, true, 303);
exit;

这种写法允许 /account/orders?id=123 一类的站内地址,但会拒绝以下内容:

https://evil.example/
http://evil.example/
javascript:alert(1)
//evil.example/
\\evil.example\

如果业务不需要查询参数,最好只允许路径,并对路径格式设置白名单。

必须允许完整 URL 时,应严格校验

有些业务需要跳转到多个受信任域名。此时必须同时校验协议、主机名和端口,不能只判断字符串是否包含某段内容,也不能仅靠后缀判断。

<?php

function getAllowedRedirectUrl(mixed $value, string $fallback): string
{
    if (!is_string($value) || filter_var($value, FILTER_VALIDATE_URL) === false) {
        return $fallback;
    }

    if (preg_match('/[\x00-\x1F\x7F]/', $value)) {
        return $fallback;
    }

    $parts = parse_url($value);

    if ($parts === false) {
        return $fallback;
    }

    $scheme = strtolower($parts['scheme'] ?? '');
    $host = strtolower(rtrim($parts['host'] ?? '', '.'));
    $port = $parts['port'] ?? null;

    $allowedHosts = [
        'www.example.com',
        'account.example.com',
    ];

    if ($scheme !== 'https') {
        return $fallback;
    }

    if (!in_array($host, $allowedHosts, true)) {
        return $fallback;
    }

    if ($port !== null && $port !== 443) {
        return $fallback;
    }

    return $value;
}

$target = getAllowedRedirectUrl(
    $_GET['redirect_to'] ?? null,
    'https://www.example.com/payment/result'
);

header('Location: ' . $target, true, 303);
exit;

不要使用下面这种判断:

str_contains($host, 'example.com')

它可能会错误接受这些主机名:

example.com.evil.test
fake-example.com

如果需要允许某个域名及其所有子域名,应明确检查主机名是否等于根域名,或者是否以“点加根域名”结尾,同时确认这些子域名都属于同一安全边界。通常,逐项列出允许的主机名更稳妥。

更可靠的设计:把跳转目标保存在服务器端

无论对 hidden input 进行编码、Base64 处理,还是通过前端 JavaScript 加以限制,它的值仍然可以被修改。创建订单时,可以将跳转目标保存在服务器端会话或订单记录中:

<?php

session_start();

$_SESSION['payment_redirects'][$orderId] = '/account/orders/' . $orderId;

支付完成并确认订单状态后,再从服务器端取出目标地址:

<?php

session_start();

$target = $_SESSION['payment_redirects'][$orderId] ?? '/payment/result';
unset($_SESSION['payment_redirects'][$orderId]);

header('Location: ' . $target, true, 303);
exit;

如果支付回调可能发生在另一个会话中,应该把目标标识保存在订单数据库中,而不是依赖浏览器 session。

支付流程还要注意

重定向参数不能用来判断“支付成功”。服务器必须通过支付平台的签名回调、订单查询接口或其他可信机制验证支付状态,不能仅凭浏览器访问了成功页面就认定支付完成。

另外还需注意:

  • hidden input 不是安全边界。来自 $_GET、$_POST 和 Cookie 的数据都应视为不可信。
  • 调用 header() 后应立即执行 exit;,防止后续代码继续运行。
  • 不要直接反射含有换行符的输入,以免造成响应头注入。
  • 不要把“签名 URL”视为唯一的解决方案。签名可以防止篡改,但如果攻击者能为任意外部 URL 获取有效签名,开放重定向漏洞仍然存在。
  • 如果目标只与订单有关,应优先传递订单 ID,再由服务器查询订单和对应的跳转规则。
  • 即使业务允许跳转到外部网站,也应显示清晰的离站提示,并且只允许跳转到经过业务确认的 HTTPS 域名。

最简单可靠的原则是:客户端只提交标识,服务器根据白名单生成最终跳转地址。

备注:内容仅供参考。