Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题: P0 (CRITICAL): - TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁 - REST 路由: __return_true → check_bearer_present 轻量鉴权 - Schema 验证: messages/tools/tool_choice 添加完整 validate_callback - SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义 - SQL 表名插值: 统一使用 %i 占位符 P1 (HIGH): - IP hash 加盐 (HMAC) + IP 解析统一 (context 共享) - Header 注入防护 (X-Request-Id/响应头 \r\n 过滤) - 数值参数范围校验 (temperature/top_p/penalties/max_tokens) - insert_key 错误处理 + revoke_key 审计日志 - Pipeline finally 保护 + RateLimiter rollback 双 store - wp_unslash 补全 + strtotime 返回值检查 - RedisRateStore $rid 冒号验证 - UpstreamStreamClient catch \Throwable P2 (MEDIUM): - AuditLogRepository DRY (提取 build_where_clause) - Token 估算 CJK 优化 (mb_strlen) - ResponseTransformMiddleware null 检查 - 嵌入响应 usage 数据修复 - SchemaManager autoload 优化 25 files changed, ~530 insertions, ~190 deletions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
171 lines
No EOL
4 KiB
PHP
171 lines
No EOL
4 KiB
PHP
<?php
|
|
/**
|
|
* Auth Middleware
|
|
*
|
|
* Pipeline stage that authenticates API gateway requests.
|
|
*
|
|
* @package WPMind\Modules\ApiGateway\Pipeline
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WPMind\Modules\ApiGateway\Pipeline;
|
|
|
|
use WPMind\Modules\ApiGateway\Auth\ApiKeyManager;
|
|
|
|
/**
|
|
* Class AuthMiddleware
|
|
*
|
|
* Authenticates requests via Bearer API key (for API endpoints)
|
|
* or Cookie+Nonce / Application Password (for management endpoints).
|
|
*/
|
|
final class AuthMiddleware implements GatewayStageInterface {
|
|
|
|
/**
|
|
* Operation prefixes that require Bearer API key authentication.
|
|
*
|
|
* @var array<int, string>
|
|
*/
|
|
private const API_OPERATION_PREFIXES = [
|
|
'chat.',
|
|
'embeddings',
|
|
'responses',
|
|
'models',
|
|
'model_detail',
|
|
'status',
|
|
];
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
public function process( GatewayRequestContext $context ): void {
|
|
if ( $this->is_api_operation( $context->operation() ) ) {
|
|
$this->authenticate_api_request( $context );
|
|
return;
|
|
}
|
|
|
|
$this->authenticate_management_request( $context );
|
|
}
|
|
|
|
/**
|
|
* Authenticate an API endpoint request via Bearer API key.
|
|
*
|
|
* @param GatewayRequestContext $context Request context.
|
|
*/
|
|
private function authenticate_api_request( GatewayRequestContext $context ): void {
|
|
$auth_header = $context->rest_request()->get_header( 'authorization' );
|
|
|
|
if ( empty( $auth_header ) ) {
|
|
$context->set_error(
|
|
new \WP_Error(
|
|
'missing_auth_header',
|
|
'Missing Authorization header.',
|
|
[ 'status' => 401 ]
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
$client_ip = $this->get_client_ip();
|
|
$result = ApiKeyManager::authenticate_bearer_header( $auth_header, $client_ip );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
$context->set_error( $result );
|
|
return;
|
|
}
|
|
|
|
$context->set_client_ip( $client_ip );
|
|
$context->set_auth_result( $result );
|
|
}
|
|
|
|
/**
|
|
* Authenticate a management endpoint request via Cookie+Nonce or Application Password.
|
|
*
|
|
* @param GatewayRequestContext $context Request context.
|
|
*/
|
|
private function authenticate_management_request( GatewayRequestContext $context ): void {
|
|
if ( ! is_user_logged_in() ) {
|
|
$context->set_error(
|
|
new \WP_Error(
|
|
'not_authenticated',
|
|
'Authentication required.',
|
|
[ 'status' => 401 ]
|
|
)
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
$context->set_error(
|
|
new \WP_Error(
|
|
'forbidden',
|
|
'Insufficient permissions.',
|
|
[ 'status' => 403 ]
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if the operation is an API endpoint (requires Bearer key).
|
|
*
|
|
* @param string $operation Operation identifier.
|
|
* @return bool
|
|
*/
|
|
private function is_api_operation( string $operation ): bool {
|
|
foreach ( self::API_OPERATION_PREFIXES as $prefix ) {
|
|
if ( str_starts_with( $operation, $prefix ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determine the client IP address from request headers.
|
|
*
|
|
* Only trusts proxy headers (X-Forwarded-For, X-Real-IP) when
|
|
* REMOTE_ADDR matches a configured trusted proxy. Otherwise
|
|
* falls back to REMOTE_ADDR to prevent IP spoofing.
|
|
*
|
|
* @return string Client IP address.
|
|
*/
|
|
private function get_client_ip(): string {
|
|
$remote_addr = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) );
|
|
|
|
if ( $remote_addr === '' ) {
|
|
return '127.0.0.1';
|
|
}
|
|
|
|
$trusted_proxies = (array) apply_filters( 'wpmind_gateway_trusted_proxies', [ '127.0.0.1', '::1' ] );
|
|
|
|
if ( in_array( $remote_addr, $trusted_proxies, true ) ) {
|
|
$proxy_headers = [ 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP' ];
|
|
|
|
foreach ( $proxy_headers as $header ) {
|
|
$value = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ?? '' ) );
|
|
|
|
if ( $value === '' ) {
|
|
continue;
|
|
}
|
|
|
|
if ( $header === 'HTTP_X_FORWARDED_FOR' ) {
|
|
$parts = explode( ',', $value );
|
|
$value = trim( $parts[0] );
|
|
}
|
|
|
|
if ( filter_var( $value, FILTER_VALIDATE_IP ) !== false ) {
|
|
return $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( filter_var( $remote_addr, FILTER_VALIDATE_IP ) !== false ) {
|
|
return $remote_addr;
|
|
}
|
|
|
|
return '127.0.0.1';
|
|
}
|
|
} |