From 4e4ef0405b726c0cb6da0971888fb1934fbcaa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=96=BB=E5=8B=87=E7=A5=A5?= <237899745@qq.com> Date: Mon, 24 Nov 2025 20:07:34 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20=E5=A2=9E=E5=BC=BA=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E9=98=B2=E6=8A=A4=EF=BC=9A=E9=98=B2=E6=AD=A2=20SSRF?= =?UTF-8?q?=20=E6=94=BB=E5=87=BB=E5=92=8C=20Token=20=E6=B3=84=E9=9C=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端安全增强: - 新增 SSRF 防护机制,验证 SFTP 目标地址 - 添加 isPrivateIp() 函数,检测并阻止连接内网地址 - 添加 validateSftpDestination() 函数,验证主机名和端口 - 支持 DNS 解析和 IP 地址验证 - 添加 SFTP 连接超时配置(默认8秒) - 移除 URL 参数中的 token 认证,只接受 Header 或 HttpOnly Cookie 前端安全改进: - 移除下载链接中的 token 参数 - 改为依赖同域 Cookie 进行身份验证 - 避免 token 在 URL 中暴露,防止日志泄露 环境变量配置: - ALLOW_PRIVATE_SFTP=true 可允许连接内网(测试环境) - SFTP_CONNECT_TIMEOUT 可配置连接超时时间 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- backend/auth.js | 4 +-- backend/server.js | 72 +++++++++++++++++++++++++++++++++++++++++++---- frontend/app.js | 10 +++---- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/backend/auth.js b/backend/auth.js index d5c65ee..a52c70c 100644 --- a/backend/auth.js +++ b/backend/auth.js @@ -51,8 +51,8 @@ function generateToken(user) { // 验证Token中间件 function authMiddleware(req, res, next) { - // 从请求头、cookie或URL参数中获取token - const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token || req.query?.token; + // 从请求头或HttpOnly Cookie获取token(不再接受URL参数以避免泄露) + const token = req.headers.authorization?.replace('Bearer ', '') || req.cookies?.token; if (!token) { return res.status(401).json({ diff --git a/backend/server.js b/backend/server.js index 0c8bfeb..1beda9d 100644 --- a/backend/server.js +++ b/backend/server.js @@ -14,6 +14,8 @@ const fs = require('fs'); const { body, validationResult } = require('express-validator'); const archiver = require('archiver'); const { exec, execSync } = require('child_process'); +const net = require('net'); +const dns = require('dns').promises; const util = require('util'); const execAsync = util.promisify(exec); @@ -626,14 +628,70 @@ function cleanupOldTempFiles() { console.error('[清理] 清理临时文件目录失败:', error.message); } } + +function isPrivateIp(ip) { + if (!net.isIP(ip)) return false; + return ip.startsWith('10.') || + ip.startsWith('192.168.') || + ip.startsWith('172.16.') || + ip.startsWith('172.17.') || + ip.startsWith('172.18.') || + ip.startsWith('172.19.') || + ip.startsWith('172.20.') || + ip.startsWith('172.21.') || + ip.startsWith('172.22.') || + ip.startsWith('172.23.') || + ip.startsWith('172.24.') || + ip.startsWith('172.25.') || + ip.startsWith('172.26.') || + ip.startsWith('172.27.') || + ip.startsWith('172.28.') || + ip.startsWith('172.29.') || + ip.startsWith('172.30.') || + ip.startsWith('172.31.') || + ip.startsWith('127.') || + ip === '0.0.0.0' || + ip === '::1' || + ip.startsWith('fe80:') || + ip.startsWith('fd'); +} + +async function validateSftpDestination(host, port) { + if (!host || typeof host !== 'string' || host.length > 255) { + throw new Error('无效的SFTP主机'); + } + if (host.includes('://') || host.includes('/')) { + throw new Error('SFTP主机不能包含协议或路径'); + } + + const portNum = parseInt(port, 10) || 22; + if (portNum < 1 || portNum > 65535) { + throw new Error('SFTP端口范围应为1-65535'); + } + + let resolvedIp; + try { + const lookup = await dns.lookup(host); + resolvedIp = lookup.address; + } catch (err) { + throw new Error('无法解析SFTP主机,请检查域名或IP'); + } + + if (isPrivateIp(resolvedIp) && process.env.ALLOW_PRIVATE_SFTP !== 'true') { + throw new Error('出于安全考虑,不允许连接内网地址'); + } + + return { host, port: portNum, resolvedIp }; +} // SFTP连接 async function connectToSFTP(config) { const sftp = new SftpClient(); await sftp.connect({ host: config.ftp_host, - port: config.ftp_port || 22, + port: parseInt(config.ftp_port, 10) || 22, username: config.ftp_user, - password: config.ftp_password + password: config.ftp_password, + readyTimeout: parseInt(process.env.SFTP_CONNECT_TIMEOUT || '8000', 10) }); return sftp; } @@ -1215,7 +1273,7 @@ app.post('/api/user/update-ftp', try { const { ftp_host, ftp_port, ftp_user, ftp_password, http_download_base_url } = req.body; - // 调试日志:查看接收到的配置 + // 调试日志:查看接收到的配置(掩码密码) console.log("[DEBUG] 收到SFTP配置:", { ftp_host, ftp_port, @@ -1224,7 +1282,6 @@ app.post('/api/user/update-ftp', http_download_base_url }); - // 如果用户已配置FTP且密码为空,使用现有密码 let actualPassword = ftp_password; if (!ftp_password && req.user.has_ftp_config && req.user.ftp_password) { @@ -1236,9 +1293,12 @@ app.post('/api/user/update-ftp', }); } + // 主机校验与防SSRF + const { port: safePort } = await validateSftpDestination(ftp_host, ftp_port); + // 验证FTP连接 try { - const sftp = await connectToSFTP({ ftp_host, ftp_port, ftp_user, ftp_password: actualPassword }); + const sftp = await connectToSFTP({ ftp_host, ftp_port: safePort, ftp_user, ftp_password: actualPassword }); await sftp.end(); } catch (error) { return res.status(400).json({ @@ -1250,7 +1310,7 @@ app.post('/api/user/update-ftp', // 更新用户配置 UserDB.update(req.user.id, { ftp_host, - ftp_port, + ftp_port: safePort, ftp_user, ftp_password: actualPassword, http_download_base_url: http_download_base_url || null, diff --git a/frontend/app.js b/frontend/app.js index 11b4aad..166d69e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -921,9 +921,9 @@ handleDragLeave(e) { ? `/${file.name}` : `${this.currentPath}/${file.name}`; - // 使用标签下载,通过URL参数传递token + // 使用标签下载,依赖同域 Cookie/凭证 const link = document.createElement('a'); - link.href = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`; + link.href = `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; link.setAttribute('download', file.name); document.body.appendChild(link); link.click(); @@ -1143,8 +1143,8 @@ handleDragLeave(e) { return file.httpDownloadUrl; } - // 本地存储或未配置HTTP URL,使用API下载 - return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}&token=${this.token}`; + // 本地存储或未配置HTTP URL,使用API下载(同域 Cookie 验证) + return `${this.apiBase}/api/files/download?path=${encodeURIComponent(filePath)}`; }, // 获取文件缩略图URL @@ -1242,7 +1242,7 @@ handleDragLeave(e) { // 使用标签下载,通过URL参数传递token,浏览器会显示下载进度 const link = document.createElement('a'); - link.href = `${this.apiBase}/api/upload/download-tool?token=${this.token}`; + link.href = `${this.apiBase}/api/upload/download-tool`; link.setAttribute('download', `玩玩云上传工具_${this.user.username}.zip`); document.body.appendChild(link); link.click();