wpbridge/includes/Security/Validator.php
feibisi fe4095db80 fix: 修复 Codex 代码评审发现的问题
HIGH 级别修复:
- AdminPage.php: 移除重复的 Logger 导入
- AdminPage.php: 修复 auth_token 双重加密问题
- AdminPage.php: 添加 action 参数白名单验证
- CacheManager.php: get_stats() 使用 $wpdb->prepare()
- source-editor.php: 不显示加密后的 token,使用占位符
- Validator.php: DNS 解析失败时视为本地地址(SSRF 防护)

MEDIUM 级别修复:
- BridgeCommand.php: 使用 WordPress 文件系统 API
- SourceModel.php: 改进 auth_token 解密回退逻辑
- SourceModel.php: 添加 URL 协议验证 (http/https)
- Encryption.php: 使用随机生成的密钥替代站点 URL 哈希

LOW 级别修复:
- BackgroundUpdater.php: 移除 PHP 8.0 联合类型语法
- Plugin.php: 使用 wp_cache_flush_group() 如果可用
- Validator.php: 添加 IPv6 私有地址检查
- uninstall.php: 清理 wpbridge_encryption_key 选项

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 19:59:13 +08:00

229 lines
5.5 KiB
PHP
Raw Permalink 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
/**
* 输入校验
*
* @package WPBridge
*/
namespace WPBridge\Security;
// 防止直接访问
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* 输入校验类
*/
class Validator {
/**
* 校验 URL
*
* @param string $url URL
* @return bool
*/
public static function is_valid_url( string $url ): bool {
if ( empty( $url ) ) {
return false;
}
// 基本格式校验
if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
return false;
}
// 只允许 http 和 https
$scheme = parse_url( $url, PHP_URL_SCHEME );
if ( ! in_array( $scheme, [ 'http', 'https' ], true ) ) {
return false;
}
// 检查是否有主机名
$host = parse_url( $url, PHP_URL_HOST );
if ( empty( $host ) ) {
return false;
}
// 禁止本地地址(安全考虑)
if ( self::is_local_address( $host ) ) {
return false;
}
return true;
}
/**
* 检查是否是本地地址
*
* @param string $host 主机名
* @return bool
*/
private static function is_local_address( string $host ): bool {
// 本地主机名
$local_hosts = [ 'localhost', '127.0.0.1', '::1' ];
if ( in_array( $host, $local_hosts, true ) ) {
return true;
}
// 私有 IP 范围
$ip = gethostbyname( $host );
if ( $ip === $host ) {
// 无法解析,为安全起见视为本地地址
return true;
}
// 检查私有 IP 范围IPv4
$private_ranges = [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8',
];
foreach ( $private_ranges as $range ) {
if ( self::ip_in_range( $ip, $range ) ) {
return true;
}
}
// 检查 IPv6 私有地址
if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 ) ) {
// fc00::/7 (Unique Local Addresses)
// fe80::/10 (Link-Local Addresses)
if ( preg_match( '/^(fc|fd|fe80)/i', $ip ) ) {
return true;
}
}
return false;
}
/**
* 检查 IP 是否在范围内
*
* @param string $ip IP 地址
* @param string $range CIDR 范围
* @return bool
*/
private static function ip_in_range( string $ip, string $range ): bool {
list( $subnet, $bits ) = explode( '/', $range );
$ip_long = ip2long( $ip );
$subnet_long = ip2long( $subnet );
$mask = -1 << ( 32 - (int) $bits );
return ( $ip_long & $mask ) === ( $subnet_long & $mask );
}
/**
* 校验版本号
*
* @param string $version 版本号
* @return bool
*/
public static function is_valid_version( string $version ): bool {
if ( empty( $version ) ) {
return false;
}
// 支持语义化版本和 WordPress 风格版本
// 例如: 1.0.0, 1.0, 1.0.0-beta, 1.0.0-rc.1
$pattern = '/^[0-9]+(\.[0-9]+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/';
return (bool) preg_match( $pattern, $version );
}
/**
* 校验 slug
*
* @param string $slug Slug
* @return bool
*/
public static function is_valid_slug( string $slug ): bool {
if ( empty( $slug ) ) {
return true; // 空 slug 是允许的(表示匹配所有)
}
// 只允许小写字母、数字、连字符
$pattern = '/^[a-z0-9-]+$/';
return (bool) preg_match( $pattern, $slug );
}
/**
* 校验 JSON 响应结构
*
* @param array $data 数据
* @param array $required 必需字段
* @return array 错误数组
*/
public static function validate_json_structure( array $data, array $required ): array {
$errors = [];
foreach ( $required as $field ) {
if ( ! isset( $data[ $field ] ) ) {
$errors[] = sprintf( __( '缺少必需字段: %s', 'wpbridge' ), $field );
}
}
return $errors;
}
/**
* 校验更新信息 JSON
*
* @param array $data 数据
* @return array 错误数组
*/
public static function validate_update_info( array $data ): array {
$errors = [];
// 必需字段
if ( empty( $data['version'] ) ) {
$errors[] = __( '缺少版本号', 'wpbridge' );
} elseif ( ! self::is_valid_version( $data['version'] ) ) {
$errors[] = __( '无效的版本号格式', 'wpbridge' );
}
// 下载 URL
$download_url = $data['download_url'] ?? $data['package'] ?? '';
if ( ! empty( $download_url ) && ! self::is_valid_url( $download_url ) ) {
$errors[] = __( '无效的下载 URL', 'wpbridge' );
}
return $errors;
}
/**
* 清理 HTML
*
* @param string $html HTML 内容
* @return string
*/
public static function sanitize_html( string $html ): string {
return wp_kses_post( $html );
}
/**
* 清理文本
*
* @param string $text 文本
* @return string
*/
public static function sanitize_text( string $text ): string {
return sanitize_text_field( $text );
}
/**
* 清理 URL
*
* @param string $url URL
* @return string
*/
public static function sanitize_url( string $url ): string {
return esc_url_raw( $url );
}
}