From 8a4dadbbc0f937c3cf40812c7e56c7eb44664039 Mon Sep 17 00:00:00 2001 From: shaw Date: Wed, 7 Jan 2026 21:55:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20=E4=BF=AE=E5=A4=8D=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E8=84=9A=E6=9C=AC=E5=8A=9F=E8=83=BD=E7=9A=84RCE?= =?UTF-8?q?=E5=92=8CSSRF=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 BALANCE_SCRIPT_ENABLED 默认值改为 false,需显式启用 - 添加 isUrlSafe() SSRF防护,禁止访问: - localhost/127.x - 私有IP (10.x, 172.16-31.x, 192.168.x) - AWS metadata (169.254.x) - 非HTTP(S)协议 --- src/services/balanceScriptService.js | 49 ++++++++++++++++++++++++++++ src/utils/featureFlags.js | 8 +++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/services/balanceScriptService.js b/src/services/balanceScriptService.js index 5bf06801..3d348d33 100644 --- a/src/services/balanceScriptService.js +++ b/src/services/balanceScriptService.js @@ -2,6 +2,50 @@ const vm = require('vm') const axios = require('axios') const { isBalanceScriptEnabled } = require('../utils/featureFlags') +/** + * SSRF防护:检查URL是否访问内网或敏感地址 + * @param {string} url - 要检查的URL + * @returns {boolean} - true表示URL安全 + */ +function isUrlSafe(url) { + try { + const parsed = new URL(url) + const hostname = parsed.hostname.toLowerCase() + + // 禁止的协议 + if (!['http:', 'https:'].includes(parsed.protocol)) { + return false + } + + // 禁止访问localhost和私有IP + const privatePatterns = [ + /^localhost$/i, + /^127\./, + /^10\./, + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, + /^192\.168\./, + /^169\.254\./, // AWS metadata + /^0\./, // 0.0.0.0 + /^::1$/, + /^fc00:/i, + /^fe80:/i, + /\.local$/i, + /\.internal$/i, + /\.localhost$/i + ] + + for (const pattern of privatePatterns) { + if (pattern.test(hostname)) { + return false + } + } + + return true + } catch { + return false + } +} + /** * 可配置脚本余额查询执行器 * - 脚本格式:({ request: {...}, extractor: function(response){...} }) @@ -55,6 +99,11 @@ class BalanceScriptService { throw new Error('脚本 request.url 不能为空') } + // SSRF防护:验证URL安全性 + if (!isUrlSafe(request.url)) { + throw new Error('脚本 request.url 不安全:禁止访问内网地址、localhost或使用非HTTP(S)协议') + } + if (typeof extractor !== 'function') { throw new Error('脚本 extractor 必须是函数') } diff --git a/src/utils/featureFlags.js b/src/utils/featureFlags.js index 35802d55..c2dd4f07 100644 --- a/src/utils/featureFlags.js +++ b/src/utils/featureFlags.js @@ -20,8 +20,9 @@ const parseBooleanEnv = (value) => { } /** - * 是否允许执行“余额脚本”(安全开关) - * 默认开启,便于保持现有行为;如需禁用请显式设置 BALANCE_SCRIPT_ENABLED=false(环境变量优先) + * 是否允许执行"余额脚本"(安全开关) + * ⚠️ 安全警告:vm模块非安全沙箱,默认禁用。如需启用请显式设置 BALANCE_SCRIPT_ENABLED=true + * 仅在完全信任管理员且了解RCE风险时才启用此功能 */ const isBalanceScriptEnabled = () => { if ( @@ -36,7 +37,8 @@ const isBalanceScriptEnabled = () => { config?.features?.balanceScriptEnabled ?? config?.security?.enableBalanceScript - return typeof fromConfig === 'boolean' ? fromConfig : true + // 默认禁用,需显式启用 + return typeof fromConfig === 'boolean' ? fromConfig : false } module.exports = {