Files
wenquanauth/function/function_core.php
T
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

492 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Created by PhpStorm.
* User: 温泉
* Date: 2017-10-15
* Time: 12:07
*/
define('IN_SYS', true);//初始化系统
error_reporting(E_ALL);
/*
* 初始化脚本
*/
session_set_cookie_params(time() + 2592000);//保持登陆30天
session_start();//开启SESSION功能
date_default_timezone_set('PRC');//设置当前时区为中国
header("Content-type: text/html; charset=utf-8");
//过滤xss代码
foreach ($_POST as $key => $value) {
$_POST[$key] = xss_clean($value);
}
foreach ($_GET as $key => $value) {
$_GET[$key] = xss_clean($value);
}
foreach ($_COOKIE as $key => $value) {
$_COOKIE[$key] = xss_clean($value);
}
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = xss_clean($value);
}
function makejson($code = 1,$msg = 'ok',$data = array()){
return json_encode(array_merge(array('code'=>intval($code),'msg'=>$msg),$data));
}
/*
* 初始化数据库链接
*/
$G = array();
include __DIR__ . '/db.class.php';
include __DIR__ . '/config.inc.php';
$return = $db = new db($G['db']['server'], $G['db']['user'], $G['db']['pass'], $G['db']['dbname']);
if (!$return) {
die('数据库链接失败,请检查设置是否正确:' . $db->geterror());
}
/*
* 读取系统相关设置
*/
$back = $db->select_all_row('sq_config', array('*'), '', '');
foreach ($back as $value) {
$G['config'][$value['setname']] = $value['setvalue'];
}
include __DIR__ . '/setname.inc.php';
foreach ($SetName as $key=>$value){
if (!isset($G['config'][$key])){
$db->insert_back_id('sq_config',array('setname'=>$key,'setvalue'=>$value));
}
}
$back = $db->select_all_row('sq_config', array('*'), '', '');
foreach ($back as $value) {
$G['config'][$value['setname']] = $value['setvalue'];
}
$G['config']['kqxt'] = numtobool($G['config']['kqxt']);
$G['config']['yjtx'] = numtobool($G['config']['yjtx']);
$G['config']['xtsy'] = numtobool($G['config']['xtsy']);
$G['config']['epay_againcheck'] = numtobool($G['config']['epay_againcheck']);
$G['sys']['rootdir'] = dirname(__DIR__);
/*
$_SESSION['auth']['checktime'] = 0;
if (empty($_SESSION['auth']['checktime']) || time() - $_SESSION['auth']['checktime'] > 3600) {
if (isset($ServerDomain)) unset($ServerDomain);
$ServerDomain['1'] = 'https://new.api.shouquan.wenquan6.cn:99/api.php';
$ServerDomain['2'] = 'https://new.api.shouquan.wenquan.cleverqq.cn:99/api.php';
$ServerDomain['3'] = 'http://new.api.shouquan.wenquan6.cn:98/api.php';
$ServerDomain['4'] = 'http://new.api.shouquan.wenquan.cleverqq.cn:98/api.php';
$ServerDomain['5'] = 'http://api.shouquan.wenquan6.cn/api.php';
$ServerDomain['6'] = 'http://api.shouquan.wenquan.cleverqq.cn/api.php';
if (isset($ServerDomain[(string)$G['config']['sid']]) && (time() - $G['config']['stime']) < 86400){
$auth = json_decode(curl_request($ServerDomain[$G['config']['sid']].'?mod=checkauth&domain=' . $_SERVER['HTTP_HOST'].'&sitekey='.$G['config']['sitekey']), true);
}
if (empty($auth['code'])) {
foreach ($ServerDomain as $key=>$value){
$auth = json_decode(curl_request($value.'?mod=checkauth&domain=' . $_SERVER['HTTP_HOST'].'&sitekey='.$G['config']['sitekey']), true);
if (!empty($auth['code'])){
updateset('sid',$key);
updateset('stime',time());
break;
}
}
}
if ($auth['code'] == '-3') {
die($auth['tips']);
}
}
if ($auth['code'] == '1'){
$_SESSION['auth']['checktime'] = time();
$ServerURL = $ServerDomain[$G['config']['sid']];
}
*/
$ServerURL = 'https://new.api.shouquan.wenquan6.cn:99/api.php';
include __DIR__ . '/ver.inc.php';
include_once __DIR__ . '/MailTipsTemplate.php';
/*
* 注册申明常用函数
*/
$http_type = ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https')) ? 'https://' : 'http://';
/*
* 一些系统提示,用alert并且跳转页面
*/
function tips($message, $url = 'javascript:history.go(-1);')
{
die('<html lang="zh">
<head>
<title>系统提示</title>
<script type="text/javascript">
alert("' . $message . '");
window.location.href=\'' . $url . '\';
</script>
</head>
<body>
</body>
</html>');
}
/*
* 获取用户的真实IP
*/
function get_real_ip()
{
$ip=FALSE;
//客户端IP 或 NONE
if(!empty($_SERVER["HTTP_CLIENT_IP"])){
$ip = $_SERVER["HTTP_CLIENT_IP"];
}
//多重代理服务器下的客户端真实IP地址(可能伪造),如果没有使用代理,此字段为空
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode (", ", $_SERVER['HTTP_X_FORWARDED_FOR']);
if ($ip) { array_unshift($ips, $ip); $ip = FALSE; }
for ($i = 0; $i < count($ips); $i++) {
if (!preg_match ("/^(10│172.16│192.168)./", $ips[$i])) {
$ip = $ips[$i];
break;
}
}
}
return ($ip ? $ip : $_SERVER['REMOTE_ADDR']);
}
/*
* 将时间戳转换为北京标准时间,如果时间戳为空则是取当前时间
*/
function Get_Date($time = '')
{
if (empty($time)) {
$time = time();
}
if (!is_numeric($time)) {
return $time;
}
return date('Y-m-d H:i:s', $time);
}
/*
* 随机字符生成
*/
function rand_str($length, $p = 'all')
{
$yznums = '0123456789';
$lowers = 'abcdefghijklmnopqrstuvwxyz';
$uppers = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
if ($p == 'all') {
$src = $yznums . $lowers . $uppers;
} else {
$src = '';
if (strpos($p, 'num') !== false)
$src .= $yznums;
if (strpos($p, 'lower') !== false)
$src .= $lowers;
if (strpos($p, 'upper') !== false)
$src .= $uppers;
}
return $src ? substr(str_shuffle($src), 0, $length) : $src;
}
/**
* 更新系统设置
*/
function updateset($name, $value)
{
global $G,$db;
$db->update('sq_config', array('setname' => $name), 'AND', array('setvalue' => $value));
$G['config'][$name] = $value;
}
/* ---------- P0-05:密码哈希(bcrypt)与旧格式兼容 ---------- */
/**
* 判断存储的密码值是否为新 bcrypt 哈希
*/
function wq_is_hashed_password($stored)
{
return is_string($stored) && strlen($stored) === 60 && strpos($stored, '$2y$') === 0;
}
/**
* 生成新密码的入库值(bcrypt)
*/
function wq_hash_password($plain)
{
return password_hash((string)$plain, PASSWORD_BCRYPT);
}
/**
* 校验密码(兼容旧格式):
* - 存储值为 bcrypt -> password_verify
* - 旧 sq_admin(md5 存储)-> $legacy='md5',与 md5(明文) 比对
* - 旧 sq_user/sq_agent(明文存储)-> 直接比对
* @param string $plain 明文密码
* @param string $stored 数据库存储值
* @param string $legacy 'plain'(用户/代理旧格式)或 'md5'(管理员旧格式)
*/
function wq_verify_password($plain, $stored, $legacy = 'plain')
{
$plain = (string)$plain;
$stored = (string)$stored;
if (wq_is_hashed_password($stored)) {
return password_verify($plain, $stored);
}
if ($legacy === 'md5') {
return hash_equals($stored, md5($plain));
}
return hash_equals($stored, $plain);
}
/*
* 布尔逻辑型转换为数字0或1
*/
function booltonum($bool)
{
if ($bool) {
return 1;
} else {
return 0;
}
}
/*
* 布尔文本型转换为数字0或1
*/
function textbooltonum($text)
{
if ($text == 'on' || $text == 'true') {
return '1';
} else {
return '0';
}
}
/*
* 数字还原为普通的布尔逻辑值
*/
function numtobool($num)
{
if ($num == 1) {
return true;
} else {
return false;
}
}
/**
* XSS暴力过滤神器,防止xss攻击
*/
function xss_clean($data)
{
// Fix &entity\n;
$data = str_replace(array('&amp;', '&lt;', '&gt;'), array('&amp;amp;', '&amp;lt;', '&amp;gt;'), $data);
$data = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $data);
$data = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $data);
$data = html_entity_decode($data, ENT_COMPAT, 'UTF-8');
// Remove any attribute starting with "on" or xmlns
$data = preg_replace('#(<[^>]+?[\x00-\x20"\'])(?:on|xmlns)[^>]*+>#iu', '$1>', $data);
// Remove javascript: and vbscript: protocols
$data = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $data);
$data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $data);
$data = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $data);
// Only works in IE: <span style="width: expression(alert('Ping!'));"></span>
$data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
$data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#i', '$1>', $data);
$data = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#iu', '$1>', $data);
// Remove namespaced elements (we do not need them)
$data = preg_replace('#</*\w+:\w[^>]*+>#i', '', $data);
// http://www.111cn.net/
do {// Remove really unwanted tags
$old_data = $data;
$data = preg_replace('#</*(?:applet|b(?:ase|gsound|link)|embed|frame(?:set)?|i(?:frame|layer)|l(?:ayer|ink)|meta|object|s(?:cript|tyle)|title|xml)[^>]*+>#i', '', $data);
} while ($old_data !== $data);
// we are done...
return $data;
}
/**
* @param $second
* @return string
*/
function time_last($second)
{
if ($second < 0) {
return '已结束';
}
$day = floor($second / 86400);
$second = $second - $day * 86400;
$hour = floor($second / 3600);
$second = $second - $hour * 3600;
$minute = floor($second / 60);
$second = $second - $minute * 60;
return $day . '天' . $hour . '时' . $minute . '分' . $second . '秒';
}
/**
* 使用CURL进行HTTP请求
* 参数1:访问的URL,
* 参数2:post数据(不填则为GET),
* 参数3:提交的$cookies,
* 参数4:是否返回$cookies
*/
function curl_request($url, $post = '', $cookie = '', $returnCookie = 0)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_USERAGENT, 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)');
curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($curl, CURLOPT_AUTOREFERER, 1);
curl_setopt($curl, CURLOPT_REFERER, "http://XXX");
if ($post) {
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
}
if ($cookie) {
curl_setopt($curl, CURLOPT_COOKIE, $cookie);
}
curl_setopt($curl, CURLOPT_HEADER, $returnCookie);
curl_setopt($curl, CURLOPT_TIMEOUT, 10);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);
$data = curl_exec($curl);
if (curl_errno($curl)) {
return '';
}
curl_close($curl);
if ($returnCookie) {
list($header, $body) = explode("\r\n\r\n", $data, 2);
preg_match_all("/Set\-Cookie:([^;]*);/", $header, $matches);
$info['cookie'] = substr($matches[1][0], 1);
$info['content'] = $body;
return $info;
} else {
return $data;
}
}
/**
* 调用温泉云平台进行邮件发送
*/
function sendemail($title, $contant, $tomail, &$msg = '', $debug = false)
{
global $G,$ServerURL;
require_once (__DIR__ . '/PHPMailer.php');
require_once (__DIR__ . '/SMTP.php');
require_once (__DIR__ . '/Exception.php');
$mail = new \PHPMailer\PHPMailer\PHPMailer();
$mail->SMTPDebug = $debug;
$mail->isSMTP();
$mail->SMTPAuth = true;
$mail->Host = $G['config']['smtp_address'];
$mail->SMTPSecure = 'ssl';
$mail->Port = $G['config']['smtp_port'];
$mail->CharSet = 'UTF-8';
$mail->FromName = $G['config']['sitename'];
$mail->Username = $G['config']['smtp_user'];
$mail->Password = $G['config']['smtp_pass'];
$mail->From = $G['config']['smtp_user'];
$mail->isHTML(true);
$mail->addAddress($tomail);
$mail->Subject = $title;
$mail->Body = $contant;
$mail->SMTPOptions = array(
'ssl' => array(
'verify_peer' => false,
'verify_peer_name' => false,
'allow_self_signed' => true
)
);
echo $mail->SMTPDebug;
$re = $mail->send();
if($re){
return true;
}else{
$msg = '邮件发送失败返回错误:'.$mail->ErrorInfo;
return false;
}
/*
foreach ($_POST as $v=>$value){
$_POST[$v] = urlencode($value);
}
$ev['server'] = urlencode($G['config']['smtp_address']);
$ev['port'] = urlencode($G['config']['smtp_port']);
$ev['username'] = urlencode($G['config']['smtp_user']);
$ev['password'] = urlencode($G['config']['smtp_pass']);
$ev['formname'] = urlencode($G['config']['sitename']);
$ev['tomail'] = urlencode($tomail);
$ev['subject'] = urlencode($title);
$ev['content'] = urlencode($contant);
//$post = "server={$ev['server']}&port={$ev['port']}&username={$ev['username']}&password={$ev['password']}&formname={$ev['formname']}&tomail={$ev['tomail']}&subject={$ev['subject']}&content={$ev['content']}";
$back = curl_request($ServerURL.'?mod=sendmail&domain=' . urlencode($_SERVER['HTTP_HOST']) . '&sitekey=' . $G['config']['sitekey'], $post);
if (!$json = json_decode($back, true)) {
$msg = '返回数据解析失败:'.$back;
return false;
} else {
if ($json['code'] != 1) {
$msg = $json['msg'];
return false;
}
$msg = 'success';
return true;
}
*/
}
function show_page_404()
{
die('404 您访问的页面不存在!');
}
function SendTipsMail($tomail,$content,&$BackMsg,$debug = false){
global $G,$MailTipsCon,$MailTipsFram,$MailTipsLine;
$c = '';
foreach ($content as $array){
$con = '';
foreach ($array['content'] as $line){
$con .= str_replace('[content]',$line,$MailTipsLine);
}
$c .= str_replace('[content]',$con,str_replace('[title]',$array['title'],$MailTipsCon));
}
$html = str_replace('[content]',$c,$MailTipsFram);
return sendemail('系统提醒 - '.$G['config']['sitename'],$html,$tomail,$BackMsg,$debug);
}
function GetOriginText($origin){
if ($origin == 1){
return '管理后台';
}else if ($origin == 2){
return '前台购买';
}else if ($origin == 3){
return '套餐卡密';
}else if ($origin == 4){
return '代理中心';
}else{
return '未知来源';
}
}