Files
youpaishidifu 24264cf320 加固-06(P0-05): 密码全部以 bcrypt 哈希入库,旧格式(明文/MD5)兼容并惰性迁移
背景:sq_user/sq_agent 密码明文入库、sq_admin 存 MD5;订单表 sq_trade.pass 明文在途;
找回密码 API 会把明文密码直接发邮件;日志记录明文密码。数据库泄露 = 全部账号密码泄露。

1. function/function_core.php 新增 3 个 helper(updateset 之后):
   - wq_is_hashed_password($v):判断存储值是否为新 bcrypt 哈希(60 长度 $2y$ 前缀)
   - wq_hash_password($plain):password_hash(PASSWORD_BCRYPT)
   - wq_verify_password($plain,$stored,$legacy):兼容 新 bcrypt / 旧明文(sq_user/sq_agent) / 旧 md5(sq_admin),
     哈希比对一律 hash_equals,恒定时间比较防时序侧信道

2. 所有落库点改为 bcrypt:
   - 用户开户漏斗 function/function_auth.php auth_add 入口幂等补哈希(上游已哈希则原样保留)
   - 代理开通漏斗 function/trade.inc.php type=4 代理落库前幂等补哈希
   - 下单点订单密码哈希入库:ajax.php(320/388/781)、agent/ajax.php(439) —— 订单表不再有明文在途密码
   - admin/ajax.php:addagent、addadmin、changeagent(mod=password)、changepassword 新密码
   - agent/ajax.php:开下级代理、changeuser(mod=password)、changeagent(mod=password)、changepassword 新密码
   - api.php:ChangePassword 写库
   - api/Communication.php 与 function/communication.php:用户登录校验成功后,旧明文惰性升级为 bcrypt

3. 所有校验点改为取行 + PHP 校验(不再 WHERE password=明文):
   - admin 登录(旧 MD5 兼容,成功即写回 bcrypt,session 与 DB 存同一新哈希保住 re-check 机制)
   - agent 密码登录(旧明文兼容,惰性迁移);agent token 登录 session 改存 DB 存储值
   - 根 ajax.php 续费 kt 密码校验(379/773)、卡密找回密码(1002)
   - api.php GetUserInfo 内联校验、getuserinfo() 校验(均带惰性迁移)

4. 找回密码改造(api.php RetrievePassword):
   密码已不可逆,原"邮件下发原密码"改为"重置为一次性随机新密码(bin2hex(random_bytes(6)))+ 哈希落库 + 仅邮件投递一次"

5. 日志/回传去明文:
   - admin 登录失败日志不再记录明文密码(改记账号)
   - admin getagentinfo 不再回传密码值(掩码 ********,经确认 UI 无回写风险)

6. 表结构:sq_admin.password varchar(33) -> varchar(255)(bcrypt 60 字符装不下)
   - wenquan_auth.sql 同步更新(新装库直接可用)
   - 已有库需执行:ALTER TABLE sq_admin MODIFY password varchar(255) NOT NULL;(本次本地库已执行)

回归(40 项全 PASS,8082 内置服务器 + 8081 nginx 双通道冒烟):
- API(c=api) 旧明文用户登录成功即升级 bcrypt;再登录幂等不错哈希;错密码 -205 被拒
- admin 旧 MD5 登录即迁移 bcrypt;再登录/错密码/日志无明文/checklogin 会话重校验均通过
- admin 新增管理员、改密(旧密码失效/新密码生效/恢复基线)
- admin 新增代理直接 bcrypt;getagentinfo 掩码;changeagent 改密哈希入库
- 旧明文代理登录即迁移 bcrypt;checklogin 通过;代理改密旧失效新生效
- admin 手工开通(funnel)明文 POST 直接存 bcrypt;API 登录新开通用户成功
- 注:测试应用 free 必须为 0(free=1 时 Login 入口直接放行,属产品设计,不走密码校验)

已知行为变化(记录):用户/代理列表按"密码"关键词搜索不再命中(存储已哈希,搜索本身即泄露向量,收紧);
列表中密码列显示 60 位哈希串而非明文;改密会使该账号现有会话失效需重新登录(与旧行为一致)。

api.php 为 2024 树中的旧入口(已被 api/Communication.php 取代),仍按同口径同步修了 4 处密码点防复活踩坑。
2026-10-10 12:25:52 +08:00

403 lines
18 KiB
PHP

<?php
/**
* Created by PhpStorm.
* User: 80071
* Date: 2018/2/8
* Time: 15:21
*/
/*
* 初始化函数库和系统
*/
include __DIR__ . '/function/function_core.php';
include __DIR__ . '/function/function_app.php';
include __DIR__ . '/function/communication.php';
$communication = new communication();
if (!$appinfo = $db->select_first_row('sq_apps','*',array('ID'=>$_GET['appid']),'AND')){
die('?');
}
$yymy = $appinfo['connectkey'];
$txmy = $appinfo['decryptkey'];
unset($appinfo['connectkey']);
unset($appinfo['decryptkey']);
$userip=get_real_ip();
/*
* 判断请求mod来区分各个功能
*/
if (empty($_GET['mod'])){
die();
}
if ($_GET['mod'] == 'GetTempKey'){
$tempkey = $communication->GET_Temp_Key();
$_SESSION['tempkey'] = $tempkey;
die($communication->str_encode($tempkey,$yymy,'qianming'));
}
if (empty($_SESSION['tempkey'])){
die('-1');
}
$sign = $communication->str_sign($_POST['DATA'],$txmy,$_SESSION['tempkey']);
if ($_GET['sign'] !== $sign){
echo ($communication->str_encode('签名校验失败!'.$sign,$yymy,$_SESSION['tempkey']));
unset($_SESSION['tempkey']);
die();
}
$decode = $communication->str_decode($_POST['DATA'],$yymy,$_SESSION['tempkey']);
if(!$postdata = json_decode($decode,true)){
echo ($communication->str_encode('Json格式貌似有错误?'.$decode,$yymy,$_SESSION['tempkey']));
unset($_SESSION['tempkey']);
die();
}
switch ($_GET['mod']){
case 'Initialization':
backmsg('1');
break;
case 'GetAppInfo':
$bool = true;
$i = 0 ;
while ($bool){
if (isset($appinfo[$i])){
unset($appinfo[$i]);
}else{
$bool = false;
}
$i ++ ;
}
backmsg('1','',$appinfo);
break;
case 'GetCustomData':
$userinfo = getuserinfo($appinfo,$postdata);
if ($appinfo['usetype'] == 'dqsj'){
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1'){
backmsg('-4','您的授权已到期,请先续费授权!');
}
}else if($appinfo['usetype'] == 'kcye'){
if ($userinfo['balance'] <= 0 && $userinfo['balance'] != '-1'){
backmsg('-5','授权余额不足,请先进行充值');
}
}else{
backmsg('-6','应用信息配置错误,请联系管理员进行处理');
}
if ($appinfo['bindip'] == '1' && !empty($userinfo['lip']) && $userinfo['lip'] !== $userip){
backmsg('-7','请在绑定的IP['.$userinfo['lip'].']上面登陆,当前IP['.$userip.']');
}
if ($appinfo['bindmac'] == '1' && !empty($userinfo['mac']) && $userinfo['mac'] !== $postdata['mac']){
backmsg('-8','请在绑定的设备['.$userinfo['mac'].']上面登陆');
}
if ($appinfo['bindqq'] == '1' && !empty($userinfo['rqq']) && $userinfo['rqq'] !== $postdata['robot']){
backmsg('-8','绑定的QQ['.$userinfo['rqq'].']与当前QQ['.$postdata['robot'].']不符');
}
backmsg('1','',array('data'=>$appinfo['data']));
break;
case 'CheckAuth':
$userinfo = getuserinfo($appinfo,$postdata);
if ($appinfo['usetype'] == 'dqsj'){
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1'){
backmsg('-4','您的授权已到期,请先续费授权!');
}
}else if($appinfo['usetype'] == 'kcye'){
if ($userinfo['balance'] <= 0){
backmsg('-5','授权余额不足,请先进行充值');
}
}else{
backmsg('-6','应用信息配置错误,请联系管理员进行处理');
}
$userip = get_real_ip();
if ($appinfo['bindip'] == '1' && !empty($userinfo['lip']) && $userinfo['lip'] !== $userip){
backmsg('-7','请在绑定的IP['.$userinfo['lip'].']上面登陆,当前IP['.$userip.']');
}
if ($appinfo['bindmac'] == '1' && !empty($userinfo['mac']) && $userinfo['mac'] !== $postdata['mac']){
backmsg('-8','请在绑定的设备['.$userinfo['mac'].']上面登陆');
}
if ($appinfo['bindqq'] == '1' && !empty($userinfo['rqq']) && $userinfo['rqq'] !== $postdata['robot']){
backmsg('-8','绑定的QQ['.$userinfo['rqq'].']与当前QQ['.$postdata['robot'].']不符');
}
if ($appinfo['onlinecheck'] == '1' && ((time() - $userinfo['htime']) < $appinfo['onlinesecond'])){
backmsg('-8','用户尚未下线!');
}
$db->update('sq_user',array('ID'=>$userinfo['ID'],'appid'=>$_GET['appid']),'AND',array('rqq'=>$postdata['robot'],'mac'=>$postdata['mac'],'lip'=>$userip,'ltime'=>time()));
backmsg('1','登陆成功!');
break;
case 'GetUserInfo':
$userip = get_real_ip();
if ($appinfo['logintype'] == 'zhmm'){
if(!$userinfo = $db->select_first_row('sq_user','*',array('appid'=>$_GET['appid'],'username'=>$postdata['user']),'AND')){
backmsg('-1','账号不存在');
}
// P0-05: 兼容新 bcrypt / 旧明文校验;旧明文校验成功后惰性升级为 bcrypt
if (!wq_verify_password($postdata['pass'], $userinfo['password'])){
backmsg('-2','您输入的密码错误');
}
if (!wq_is_hashed_password($userinfo['password'])){
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('password'=>wq_hash_password($postdata['pass'])));
}
}else if ($appinfo['logintype'] == 'kmsq'){
if(!$userinfo = $db->select_first_row('sq_user','*',array('appid'=>$_GET['appid'],'username'=>$postdata['user']),'AND')){
backmsg('-1','授权卡密不存在');
}
}else if ($appinfo['logintype'] == 'jcbd'){
$where['appid'] = $_GET['appid'];
if ($appinfo['bindip'] == '1'){
$where['lip'] = $userip;
}
if ($appinfo['bindmac'] == '1'){
$where['mac'] = $postdata['mac'];
}
if ($appinfo['bindqq'] == '1'){
$where['rqq'] = $postdata['robot'];
}
if(!$userinfo = $db->select_first_row('sq_user','username,mac,rip,uqq,mail,ltime,balance,rqq,lip,ltime',$where,'AND')){
backmsg('-1','未查询到您的授权');
}
}
backmsg('1','',$userinfo);
break;
case 'OnlineHeart':
$userinfo = getuserinfo($appinfo,$postdata);
if ($appinfo['usetype'] == 'dqsj'){
if ($userinfo['balance'] < time() && $userinfo['balance'] != '-1'){
backmsg('-4','您的授权已到期,请先续费授权!');
}
}else if($appinfo['usetype'] == 'kcye'){
if ($userinfo['balance'] <= 0 && $userinfo['balance'] != '-1'){
backmsg('-5','授权余额不足,请先进行充值');
}
}else{
backmsg('-6','应用信息配置错误,请联系管理员进行处理');
}
if ($appinfo['bindip'] == '1' && !empty($userinfo['lip']) && $userinfo['lip'] !== $userip){
backmsg('-7','请在绑定的IP['.$userinfo['lip'].']上面登陆,当前IP['.$userip.']');
}
if ($appinfo['bindmac'] == '1' && !empty($userinfo['mac']) && $userinfo['mac'] !== $postdata['mac']){
backmsg('-8','请在绑定的设备['.$userinfo['mac'].']上面登陆');
}
if ($appinfo['bindqq'] == '1' && !empty($userinfo['rqq']) && $userinfo['rqq'] !== $postdata['robot']){
backmsg('-8','绑定的QQ['.$userinfo['rqq'].']与当前QQ['.$postdata['robot'].']不符');
}
if ($appinfo['usetype'] == 'kcye'){
$time = time();
$chazhi = time()-$userinfo['lastreducetime'];
if ($chazhi > $appinfo['onlinesecond']+120){
//新登录
$lastbalance = $userinfo['balance'] - $appinfo['cycreduce'];
$lastreducetime = $time;
}else{
//在线减
$oninemin = (int)($chazhi/60);
$lastbalance = $userinfo['balance'] - $appinfo['cycreduce']*$oninemin;
$lastreducetime = $userinfo['lastreducetime'] - $oninemin*60;
}
$db->update('sq_user',array('ID'=>$userinfo['ID'],'appid'=>$_GET['appid']),'AND',array('htime'=>time(),'balance'=>$lastbalance,'lastreducetime'=>$lastreducetime));
}else{
$db->update('sq_user',array('ID'=>$userinfo['ID'],'appid'=>$_GET['appid']),'AND',array('htime'=>time()));
}
backmsg('1',time());
break;
case 'Unbundling':
if ($appinfo['logintype'] == 'jcbd'){
backmsg('-12','检查绑定方式登陆的授权不允许解除绑定');
}
$userinfo = getuserinfo($appinfo,$postdata);
if ($userinfo['mailcode']!== $postdata['vcode']){
backmsg('-13','验证码输入错误,请重新输入');
}
if ($userinfo['sendtime'] < time()-600){
backmsg('-13','验证码已过期,请尝试重新获取');
}
if ($appinfo['usetype'] == 'dqsj'){
if ($userinfo['balance']-$appinfo['unbindreduce'] < time() && $userinfo['balance'] != '-1'){
backmsg('-4','授权时长不足,无法解绑');
}
}else if($appinfo['usetype'] == 'kcye'){
if ($userinfo['balance']-$appinfo['unbindreduce'] <= 0 && $userinfo['balance'] != '-1'){
backmsg('-5','授权余额不足,无法解绑');
}
}else{
backmsg('-6','应用信息配置错误,请联系管理员进行处理');
}
if ($userinfo['balance'] != '-1'){
$lastbalance = $userinfo['balance'] - $appinfo['unbindreduce'];
}else{
$lastbalance = '-1';
}
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('mailcode'=>'','sendtime'=>0,'mac'=>'','lip'=>'','rqq'=>'','balance'=>$lastbalance));
if ($appinfo['usetype'] == 'dqsj'){
backmsg('1',"解除绑定成功,扣除{$appinfo['unbindreduce']}分钟授权");
}else{
backmsg('1',"解除绑定成功,扣除{$appinfo['unbindreduce']}点余额");
}
break;
case 'ReplacBinding':
$userinfo = getuserinfo($appinfo,$postdata);
if ($userinfo['mailcode']!== $postdata['vcode']){
backmsg('-13','验证码输入错误,请重新输入');
}
if ($userinfo['sendtime'] < time()-600){
backmsg('-13','验证码已过期,请尝试重新获取');
}
if ($appinfo['usetype'] == 'dqsj'){
if ($userinfo['balance']-$appinfo['unbindreduce'] < time() && $userinfo['balance'] != '-1'){
backmsg('-4','授权时长不足,无法换绑');
}
}else if($appinfo['usetype'] == 'kcye'){
if ($userinfo['balance']-$appinfo['unbindreduce'] <= 0 && $userinfo['balance'] != '-1'){
backmsg('-5','授权余额不足,无法换绑');
}
}else{
backmsg('-6','应用信息配置错误,请联系管理员进行处理');
}
if ($userinfo['balance'] != '-1'){
$lastbalance = $userinfo['balance'] - $appinfo['unbindreduce'];
}else{
$lastbalance = '-1';
}
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('mailcode'=>'','sendtime'=>0,'mac'=>$postdata['newmac'],'lip'=>$postdata['newip'],'rqq'=>$postdata['newrobot'],'balance'=>$lastbalance));
if ($appinfo['usetype'] == 'dqsj'){
backmsg('1',"更换绑定成功,扣除{$appinfo['unbindreduce']}分钟授权");
}else{
backmsg('1',"更换绑定成功,扣除{$appinfo['unbindreduce']}点余额");
}
break;
case 'ChangePassword':
if ($appinfo['logintype'] !== 'zhmm'){
backmsg('-13','非账号密码方式授权无法修改密码');
}
$userinfo = getuserinfo($appinfo,$postdata);
if ($userinfo['mailcode']!== $postdata['vcode']){
backmsg('-13','验证码输入错误,请重新输入');
}
if ($userinfo['sendtime'] < time()-600){
backmsg('-13','验证码已过期,请尝试重新获取');
}
// P0-05: 新密码以 bcrypt 哈希入库
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('password'=>wq_hash_password($postdata['newpass'])));
backmsg('1');
break;
case 'SubscriberDownline':
$userinfo = getuserinfo($appinfo,$postdata);
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('htime'=>0));
backmsg('1');
break;
case 'BalanceRecharge':
$userinfo = getuserinfo($appinfo,$postdata);
if ($userinfo['balance'] == '-1'){
backmsg('-15','您的授权已是永久授权,无需续费!');
}
if (!$keyinfo = $db->select_first_row('sq_key','*',array('kami'=>$postdata['key']),'AND')){
backmsg('-13','您输入的卡密不存在');
}
if ($keyinfo['lastmoney'] < $postdata['money']){
backmsg('-13','卡密余额不足');
}
if ($appinfo['usetype'] == 'dqsj'){ //当授权方式为到期时间
if ($userinfo['balance'] < time()){ //判断是否已经过期,如果 用户的到期时间小于当前时间
$newbalance = time() + $appinfo['rechargeface'] * $postdata['money'] *60;
//新到期时间 = 当前时间 + 充值面值 * 充值数量 * 60
}else{
$newbalance = $userinfo['balance'] + $appinfo['rechargeface'] * $postdata['money']*60;
//否则新到期时间 = 原有时间 + 充值面值 * 充值数量 *60
}
}else if ($appinfo['usetype'] == 'kcye'){
$newbalance = $userinfo['balance'] + $appinfo['rechargeface'] * $postdata['money'];
}
$newmoney = $keyinfo['lastmoney'] - $postdata['money'];
if ($keyinfo['firstusetime'] == 0){
$newinfo['firstusetime'] = time();
}
$newinfo['lastmoney'] = $newmoney;
$newinfo['lastusetime'] = time();
$db->update('sq_key',array('ID'=>$keyinfo['ID']),'AND',$newinfo);
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('balance'=>$newbalance));
backmsg('1');
break;
case 'RegisteredAccount':
$accessurl = 'http://'.$G['config']['pay_domain'].'/ajax.php?mod=register&username='.urlencode($postdata['user']).'&pass='.urlencode($postdata['pass']).'&mail='.urlencode($postdata['mail']);
$tittle = '';
break;
case 'GetVerifyingCode':
$userinfo = getuserinfo($appinfo,$postdata);
if (time()-$userinfo['sendtime'] < 60){
backmsg('-1','邮件发送过于频繁,请耐心等待'.(time()-$userinfo['sendtime']).'秒后发送');
}
$code = rand_str(6);
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('mailcode'=>$code,'sendtime'=>time()));
if(!$back = sendemail('验证码 - '.$appinfo['appname'],'['.$userinfo['username'].']您正在进行敏感操作,您本次操作的验证码为:'.$code,$userinfo['mail'])){
backmsg('-1',$back);
}
backmsg('1');
break;
case 'RetrievePassword':
$userinfo = getuserinfo($appinfo,$postdata);
if (time()-$userinfo['sendtime'] < 60){
backmsg('-1','邮件发送过于频繁,请耐心等待'.(time()-$userinfo['sendtime']).'秒后发送');
}
// P0-05: 密码已以 bcrypt 哈希入库,原明文不再可取出,也禁止邮件外发。
// 改为"重置为一次性随机新密码,仅通过用户本人邮箱投递一次"
$newpass = bin2hex(random_bytes(6));
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('password'=>wq_hash_password($newpass),'sendtime'=>time()));
sendemail('密码重置 - '.$appinfo['appname'],'['.$userinfo['username'].']您的密码已重置,新密码为:'.$newpass.',请立即登录并尽快修改密码。',$userinfo['mail']);
backmsg('1');
break;
}
function backmsg($code,$message='',$array = array()){
global $communication;
global $yymy;
$back = $array;
$back['code'] = $code;
$back['msg'] = $message;
echo ($communication->str_encode(json_encode($back),$yymy,$_SESSION['tempkey']));
unset($_SESSION['tempkey']);
die();
}
function getuserinfo($appinfo,$postdata){
$userip = get_real_ip();
global $db;
if ($appinfo['logintype'] == 'zhmm'){
if(!$userinfo = $db->select_first_row('sq_user','*',array('appid'=>$_GET['appid'],'username'=>$postdata['user']),'AND')){
backmsg('-1','账号不存在');
}
// P0-05: 兼容新 bcrypt / 旧明文校验;旧明文校验成功后惰性升级为 bcrypt
if (!wq_verify_password($postdata['pass'], $userinfo['password'])){
backmsg('-2','您输入的密码错误');
}
if (!wq_is_hashed_password($userinfo['password'])){
$db->update('sq_user',array('ID'=>$userinfo['ID']),'AND',array('password'=>wq_hash_password($postdata['pass'])));
}
}else if ($appinfo['logintype'] == 'kmsq'){
if(!$userinfo = $db->select_first_row('sq_user','*',array('appid'=>$_GET['appid'],'username'=>$postdata['user']),'AND')){
backmsg('-1','授权卡密不存在');
}
}else if ($appinfo['logintype'] == 'jcbd'){
$where['appid'] = $_GET['appid'];
if ($appinfo['bindip'] == '1'){
$where['lip'] = $userip;
}
if ($appinfo['bindmac'] == '1'){
$where['mac'] = $postdata['mac'];
}
if ($appinfo['bindqq'] == '1'){
$where['rqq'] = $postdata['robot'];
}
if(!$userinfo = $db->select_first_row('sq_user','*',$where,'AND')){
backmsg('-1','未查询到您的授权');
}
}
if ($userinfo['status'] != '1'){
backmsg('-3','用户状态不正常,请联系客服处理');
}
return $userinfo;
}