Files
youpaishidifu e6f6188ab8 加固-09(P0-07): 支付加固——Host 头投毒、卡密/代理余额原子核销、epay 库 PHP8 致命、回调落库幂等
1. Host 头投毒(R8):payment.php 的回调/跳转 URL 原用 $_SERVER['HTTP_HOST']
   拼接,被污染的 Host 头可使 ePay notify/return 回调指向攻击者域名
   - function/function_core.php 新增 wq_site_base_url():host 优先取配置
     weburl 的 scheme+host 部分(配置由管理员维护,非攻击面),仅配置缺失
     时回退 HTTP_HOST;路径取当前请求挂载目录并归一化 Windows dirname 的
     反斜杠(兼容子目录部署)
   - payment.php 免费订单跳转、czkm/yezf 支付成功跳转、mqyturn 的
     notify/return 回调 URL 全部改用该 helper

2. 卡密/代理余额原子核销(双花消除):payment.php 的 czkm(卡密)与
   yezf(代理余额)原为「读取-校验-写回」,并发下同一卡密/余额可被
   重复扣减
   - 订单占用:条件 UPDATE(WHERE status='1'),affected_num()==0 即
     中止,防同订单并发重复支付
   - 卡密核销:UPDATE ... WHERE kami=? AND status='1' AND lastmoney>=?
     ,前置条件并入 WHERE;affected_num()==0 时回滚订单占用并提示重试
   - 代理余额:UPDATE ... WHERE status='1' AND money>=? 同理
   - 顺带修复 yezf 原 bug:allspend 只读进内存从未落库,现随余额
     原子累加

3. epay 库 PHP8 致命(R10):pay/epay/lib/epay_core.function.php
   - 两处 get_magic_quotes_gpc()(PHP8 已移除,调用即致命)改为
     function_exists 守卫
   - createLinkstringUrlencode 的 count($arg)(对字符串,PHP8 TypeError)
     改为 strlen($arg)-1(与 PHP7 下实际行为 substr(0,-1) 等价)
   - 已在 PHP 8.2 实测:两函数正常拼接/验签准备,无致命

4. pay/epay/notify_url.php / return_url.php:
   - 移除 notify_url.php 中用未定义 $lastmoney/$allspend 更新代理表的
     语句(PHP7 下 null 隐式转空串,回调时把代理余额与 allspend 清零;
     与 return_url.php 的既有行为对齐)
   - 订单状态落库改为条件 UPDATE(WHERE status='1'),并发回调重复
     落库防护;status=2 分支保持幂等 success
   - return_url.php 对称条件化

回归(P0-07 全 PASS,8082 内置服务器 + 8081 nginx 双通道冒烟):
- 免费订单/czkm/yezf 跳转 URL 全部使用配置 host;Host:evil.example.com
  投毒无效
- czkm 正常核销、余额不足拒绝;双进程并发两订单恰好一个成功,
  卡密余额恰扣一次(无双花),核销日志 +1
- yezf 正常扣减 100->70;并发双订单恰好一个成功,余额 70->30 只扣
  一次,allspend 只累加一次
- epay 库 PHP 8.2 下 createLinkstring / createLinkstringUrlencode 无致命
- 注:本地内置服务器为单 worker,并发请求实际串行处理;双花消除的
  保证来自 WHERE 原子前置条件(真实多 worker 并发下亦至多一个
  UPDATE 满足条件)
2026-10-10 13:07:10 +08:00

192 lines
8.4 KiB
PHP

<?php
/**
* Created by PhpStorm.
* User: 80071
* Date: 2018/2/5
* Time: 20:17
*/
include __DIR__ . '/function/function_core.php';
error_reporting(E_ALL);
if (empty($_GET['tradeno'])) {
tips('订单号为空!');
}
if (!$tradinfo = $db->select_first_row('sq_trade', '*', array('tradeno' => $_GET['tradeno']), 'AND')) {
tips('订单信息拉取失败,无法继续支付');
}
if ($tradinfo['status'] != '1') {
tips('订单状态不正常,也许是已经支付成功了');
}
if ($tradinfo['paymoney'] == 0) {
$time = time();
$db->update('sq_trade', array('tradeno' => $_GET['tradeno']), 'AND', array('overtime' => $time, 'status' => '2'));
tips('该订单为免费订单,点击确定跳转到结果页...', wq_site_base_url() . "/payresult.php?tradeno=" . $_GET['tradeno']);
}
if ($tradinfo['paytype'] == 'zxzf') {
if ($tradinfo['onlinepaytype'] === 'zfb') {
if ($G['config']['alipaytype'] == '2') {
tips('支付宝在线支付渠道目前已关闭,请选择其他支付方式!');
}
if ($G['config']['alipaytype'] == '0') {
//免签约支付echo '111';
mqyturn('alipay');
}
if ($G['config']['alipaytype'] == '1') {
//支付宝签约支付
}
}
if ($tradinfo['onlinepaytype'] === 'wx') {
if ($G['config']['wxpaytype'] == '2') {
tips('微信在线支付渠道目前已关闭,请选择其他支付方式!');
}
if ($G['config']['wxpaytype'] == '0') {
//免签约支付
mqyturn('wxpay');
}
if ($G['config']['wxpaytype'] == '1') {
//微信签约支付
}
}
if ($tradinfo['onlinepaytype'] === 'qq') {
if ($G['config']['qqpaytype'] == '2') {
tips('QQ钱包在线支付渠道目前已关闭,请选择其他支付方式!');
}
if ($G['config']['qqpaytype'] == '0') {
//免签约支付
mqyturn('qqpay');
}
if ($G['config']['qqpaytype'] == '1') {
//QQ签约支付
}
}
tips('What the fuck?');
}
if ($tradinfo['paytype'] == 'czkm') {
$usekey = $tradinfo['kami'];
if (!$keyinfo = $db->select_first_row('sq_key', '*', array('kami' => $usekey), 'AND')) {
tips('卡密不存在!');
}
if ($keyinfo['lastmoney'] < $tradinfo['paymoney']) {
tips('卡密余额不足!');
}
if ($keyinfo['status'] != '1') {
tips('卡密已被封禁,无法使用!');
}
$time = time();
$paymoney = (float)$tradinfo['paymoney'];
// P0-07: 原子占用订单(status=1 -> 2),防同一订单被并发重复支付
if (!$db->query("UPDATE sq_trade SET overtime=" . $time . ", status='2' WHERE tradeno='" . addslashes($_GET['tradeno']) . "' AND status='1'")) {
tips('系统内部错误!');
}
if ((int)$db->affected_num() === 0) {
tips('订单状态已变化(可能已支付成功),请勿重复操作');
}
// P0-07: 原子核销卡密:前置条件(未被封禁/余额充足)并入 WHERE,消除「先读后写」双花窗口
if (!$db->query("UPDATE sq_key SET lastmoney=lastmoney-" . $paymoney . ", lastusetime=" . $time . ", firstusetime=IF(firstusetime=0," . $time . ",firstusetime) WHERE kami='" . addslashes($usekey) . "' AND status='1' AND lastmoney>=" . $paymoney)) {
tips('系统内部错误!');
}
if ((int)$db->affected_num() === 0) {
// 卡密被并发核销或已封禁:回滚订单占用,提示重试
$db->query("UPDATE sq_trade SET status='1' WHERE tradeno='" . addslashes($_GET['tradeno']) . "' AND status='2' AND overtime=" . $time);
tips('卡密状态已变化(可能已被并发使用或余额不足),请重试');
}
$lastmoney = $keyinfo['lastmoney'] - $paymoney;
$db->insert_back_id('sq_log_kami', array('keyid' => $keyinfo['ID'], 'type' => 'info', 'spendmoney' => $tradinfo['paymoney'], 'lastmoney' => $lastmoney, 'time' => $time, 'object' => $tradinfo['user'], 'IP' => get_real_ip(), 'msg' => $tradinfo['name']));
tips('已成功使用卡密进行支付,点击确认跳转至结果页面...', wq_site_base_url() . "/payresult.php?tradeno=" . $_GET['tradeno']);
}
if ($tradinfo['paytype'] == 'yezf') {
if (empty($tradinfo['agentid']) || $tradinfo['agentid'] == 0) {
tips('错误的付款方式,请重新选择!');
}
if (!$agentinfo = $db->select_first_row('sq_agent', '*', array('ID' => $tradinfo['agentid']), 'AND')) {
tips('代理信息获取失败,无法继续支付!');
}
if (($_SESSION['agent_username'] != $agentinfo['username']) || ($_SESSION['agent_password'] != $agentinfo['password'])) {
tips('当前登录的代理账号或密码与订单代理或密码不匹配!');
}
if ($agentinfo['status'] != '1') {
tips('代理状态不正常,无法继续支付!');
}
if ($agentinfo['money'] < $tradinfo['paymoney']) {
tips('很抱歉,您的余额不足,无法支付!');
}
$time = time();
$paymoney = (float)$tradinfo['paymoney'];
// P0-07: 原子占用订单(status=1 -> 2),防同一订单被并发重复支付
if (!$db->query("UPDATE sq_trade SET overtime=" . $time . ", status='2' WHERE tradeno='" . addslashes($_GET['tradeno']) . "' AND status='1'")) {
tips('系统内部错误!');
}
if ((int)$db->affected_num() === 0) {
tips('订单状态已变化(可能已支付成功),请勿重复操作');
}
// P0-07: 原子扣除代理余额(状态/余额前置条件并入 WHERE),消除「先读后写」双花窗口
// 同步修复原 bug:allspend 只读进内存从未落库,此处一并原子累加
if (!$db->query("UPDATE sq_agent SET money=money-" . $paymoney . ", allspend=allspend+" . $paymoney . " WHERE ID=" . (int)$tradinfo['agentid'] . " AND status='1' AND money>=" . $paymoney)) {
tips('系统内部错误!');
}
if ((int)$db->affected_num() === 0) {
// 代理被并发冻结或余额不足:回滚订单占用
$db->query("UPDATE sq_trade SET status='1' WHERE tradeno='" . addslashes($_GET['tradeno']) . "' AND status='2' AND overtime=" . $time);
tips('代理状态已变化(被冻结或余额不足),请重试');
}
$lastmoney = $agentinfo['money'] - $paymoney;
$db->insert_back_id('sq_log_agent', array('time' => $time, 'aid' => $agentinfo['ID'], 'ip' => get_real_ip(), 'msg' => '使用余额支付' . $tradinfo['paymoney'] . '元购买' . $tradinfo['name'] . ',余额:' . $lastmoney));
tips('您已成功使用余额付款,点击确认跳转至结果页...', wq_site_base_url() . "/payresult.php?tradeno=" . $_GET['tradeno']);
}
tips('What the fuck?');
function mqyturn($paytype)
{
global $G;
global $tradinfo;
echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>正在跳转到支付页面,请稍后</title>
</head>
';
require_once(__DIR__ . "/pay/epay/lib/epay_submit.class.php");
$alipay_config['partner'] = $G['config']['epay_pid'];
$alipay_config['key'] = $G['config']['epay_key'];
$alipay_config['sign_type'] = strtoupper('MD5');
$alipay_config['input_charset'] = strtolower('utf-8');
$alipay_config['transport'] = 'http';
$alipay_config['apiurl'] = 'http://' . $G['config']['pay_domain'] . '/';
// P0-07: 回调/回跳 URL 的 host 取自配置域名(weburl),防 HTTP_HOST 头投毒
$notify_url = str_replace("\\", '/', str_replace("\/", '/', wq_site_base_url() . "/pay/epay/notify_url.php"));
$return_url = str_replace("\\", '/', str_replace("\/", '/', wq_site_base_url() . "/pay/epay/return_url.php"));
$out_trade_no = $_GET['tradeno'];
$type = $paytype;
$name = $tradinfo['name'];
$money = $tradinfo['paymoney'];
$sitename = $G['config']['sitename'];
$parameter = array(
"pid" => trim($alipay_config['partner']),
"type" => $type,
"notify_url" => $notify_url,
"return_url" => $return_url,
"out_trade_no" => $out_trade_no,
"name" => $name,
"money" => $money,
"sitename" => $sitename
);
$alipaySubmit = new AlipaySubmit($alipay_config);
$html_text = $alipaySubmit->buildRequestForm($parameter);
echo $html_text;
echo '
</body>
</html>';
die();
}