mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-07 15:08:33 +08:00
fix: 优化本地路径构造
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkimagepickercontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||
"state" : {
|
||||
"branch" : "4.3.9",
|
||||
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkphotogallery",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "sdwebimage",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||
"state" : {
|
||||
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||
"version" : "5.21.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftygif",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||
"state" : {
|
||||
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||
"version" : "5.4.5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "tocropviewcontroller",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||
"state" : {
|
||||
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||
"version" : "2.8.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 2
|
||||
}
|
||||
+10
-10
@@ -386,10 +386,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.20.3"
|
||||
version: "0.20.2"
|
||||
isolate_contactor:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -490,10 +490,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
|
||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.12.20"
|
||||
version: "0.12.19"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -506,10 +506,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -886,10 +886,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.7.12"
|
||||
version: "0.7.11"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1016,10 +1016,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
|
||||
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.2.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -7,6 +7,12 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { sftpCacheDir } from '../config/index.js'
|
||||
import { createSecureWs } from '../utils/ws-tool.js'
|
||||
import { HostListDB, FavoriteSftpDB } from '../utils/db-class.js'
|
||||
import {
|
||||
assertPathInside,
|
||||
resolvePathInside,
|
||||
validateRemoteFileName,
|
||||
validateTaskId
|
||||
} from '../utils/sftp-cache-path.js'
|
||||
import { getConnectionOptions, handleProxyAndJumpHostConnection } from './terminal.js'
|
||||
const hostListDB = new HostListDB().getInstance()
|
||||
const favoriteSftpDB = new FavoriteSftpDB().getInstance()
|
||||
@@ -18,6 +24,22 @@ function shellEscape(s) {
|
||||
return "'" + s.replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
function removeCacheFileSync(filePath) {
|
||||
if (!filePath) return
|
||||
const safeFilePath = assertPathInside(sftpCacheDir, filePath)
|
||||
if (fs.existsSync(safeFilePath)) fs.unlinkSync(safeFilePath)
|
||||
}
|
||||
|
||||
async function removeCacheFile(filePath) {
|
||||
if (!filePath) return
|
||||
const safeFilePath = assertPathInside(sftpCacheDir, filePath)
|
||||
try {
|
||||
await fs.unlink(safeFilePath)
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Buffer 解码为字符串
|
||||
* @param {Buffer} buffer - 要解码的 Buffer
|
||||
@@ -581,17 +603,22 @@ const listenAction = (sftpClient, socket) => {
|
||||
})
|
||||
|
||||
// 下载功能
|
||||
socket.on('download_request', async ({ dirPath, targets }) => {
|
||||
socket.on('download_request', async ({ dirPath, targets } = {}) => {
|
||||
let remoteTarPath = null // 跟踪远程临时文件路径
|
||||
let taskId = null // 声明在外层以便错误处理时访问
|
||||
try {
|
||||
if (!targets || targets.length === 0) {
|
||||
if (typeof dirPath !== 'string' || !Array.isArray(targets) || targets.length === 0) {
|
||||
throw new Error('未选择要下载的文件')
|
||||
}
|
||||
|
||||
taskId = Date.now() + '-' + Math.random().toString(36).slice(2)
|
||||
const taskDir = rawPath.join(sftpCacheDir, taskId)
|
||||
await fs.ensureDir(taskDir)
|
||||
targets.forEach(target => {
|
||||
if (!target || typeof target !== 'object') throw new Error('下载目标非法')
|
||||
validateRemoteFileName(target.name)
|
||||
})
|
||||
|
||||
taskId = uuidv4()
|
||||
const taskDir = resolvePathInside(sftpCacheDir, taskId)
|
||||
await fs.ensureDir(taskDir, { mode: 0o700 })
|
||||
|
||||
const abortController = new AbortController()
|
||||
downloadTasks.set(taskId, {
|
||||
@@ -612,7 +639,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
// 文件夹:先在远端打包
|
||||
const tarFileName = `${ target.name }.tar.gz`
|
||||
remoteTarPath = `/tmp/${ taskId }.tar.gz`
|
||||
const localTarPath = rawPath.join(taskDir, tarFileName)
|
||||
const localTarPath = resolvePathInside(taskDir, tarFileName)
|
||||
|
||||
// 保存远程文件路径到任务中
|
||||
downloadTasks.get(taskId).remoteTarPath = remoteTarPath
|
||||
@@ -648,7 +675,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
socket.emit('download_ready', { taskId, fileName: tarFileName })
|
||||
} else {
|
||||
// 单文件:直接下载
|
||||
const localFilePath = rawPath.join(taskDir, target.name)
|
||||
const localFilePath = resolvePathInside(taskDir, target.name)
|
||||
|
||||
// 获取文件大小
|
||||
const statResult = await sftpClient.stat(srcPath)
|
||||
@@ -662,7 +689,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
// 多文件逻辑:打包所有选中的文件/文件夹
|
||||
const archiveName = `selected-files-${ Date.now() }.tar.gz`
|
||||
remoteTarPath = `/tmp/${ taskId }.tar.gz`
|
||||
const localTarPath = rawPath.join(taskDir, archiveName)
|
||||
const localTarPath = resolvePathInside(taskDir, archiveName)
|
||||
|
||||
// 保存远程文件路径到任务中
|
||||
downloadTasks.get(taskId).remoteTarPath = remoteTarPath
|
||||
@@ -991,9 +1018,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
|
||||
// 生成缓存文件名(使用UUID避免猜测和冲突)
|
||||
const fileName = rawPath.basename(filePath)
|
||||
const cacheFileName = `${ uuidv4() }_${ fileName }`
|
||||
const localImagePath = rawPath.join(sftpCacheDir, cacheFileName)
|
||||
const fileName = rawPath.posix.basename(filePath)
|
||||
const cacheFileName = `${ uuidv4() }.${ ext }`
|
||||
const localImagePath = resolvePathInside(sftpCacheDir, cacheFileName)
|
||||
|
||||
logger.info(`开始下载图片到缓存: ${ filePath } -> ${ localImagePath }`)
|
||||
|
||||
@@ -1055,17 +1082,20 @@ const listenAction = (sftpClient, socket) => {
|
||||
// -------- 上传相关功能 --------
|
||||
|
||||
// 开始上传
|
||||
socket.on('upload_start', async ({ taskId, fileName, fileSize, targetPath }) => {
|
||||
socket.on('upload_start', async ({ taskId, fileName, fileSize, targetPath } = {}) => {
|
||||
try {
|
||||
logger.info(`收到上传请求: ${ fileName }, 大小: ${ (fileSize / 1024 / 1024 / 1024).toFixed(2) }GB`)
|
||||
|
||||
if (!taskId || !fileName || !fileSize || !targetPath) {
|
||||
validateTaskId(taskId)
|
||||
validateRemoteFileName(fileName)
|
||||
if (!Number.isSafeInteger(fileSize) || fileSize <= 0 || typeof targetPath !== 'string' || !targetPath) {
|
||||
throw new Error('上传参数不完整')
|
||||
}
|
||||
if (uploadTasks.has(taskId)) throw new Error('上传任务已存在')
|
||||
|
||||
// 创建临时文件路径(清理文件名中的特殊字符)
|
||||
const safeFileName = fileName.replace(/[<>:"|?*]/g, '_')
|
||||
const tempFilePath = rawPath.join(sftpCacheDir, `temp_${ taskId }_${ safeFileName }`)
|
||||
// 本地缓存名完全由服务端生成,客户端参数不得参与本地路径构造
|
||||
fs.ensureDirSync(sftpCacheDir, { mode: 0o700 })
|
||||
const tempFilePath = resolvePathInside(sftpCacheDir, `.upload-${ uuidv4() }.part`)
|
||||
// 创建上传任务
|
||||
const uploadTask = {
|
||||
taskId,
|
||||
@@ -1093,8 +1123,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
})
|
||||
|
||||
// 上传文件分片
|
||||
socket.on('upload_chunk', async ({ taskId, chunkIndex, chunkData, totalChunks, isLastChunk }) => {
|
||||
socket.on('upload_chunk', async ({ taskId, chunkIndex, chunkData, totalChunks, isLastChunk } = {}) => {
|
||||
try {
|
||||
validateTaskId(taskId)
|
||||
const task = uploadTasks.get(taskId)
|
||||
|
||||
if (!task) {
|
||||
@@ -1110,7 +1141,11 @@ const listenAction = (sftpClient, socket) => {
|
||||
// 确保缓存目录存在
|
||||
fs.ensureDirSync(sftpCacheDir)
|
||||
|
||||
task.writeStream = fs.createWriteStream(task.tempFilePath)
|
||||
const safeTempFilePath = assertPathInside(sftpCacheDir, task.tempFilePath)
|
||||
task.writeStream = fs.createWriteStream(safeTempFilePath, {
|
||||
flags: 'wx',
|
||||
mode: 0o600
|
||||
})
|
||||
task.totalChunks = totalChunks
|
||||
|
||||
// 处理写入流错误(将错误标记到任务中)
|
||||
@@ -1196,6 +1231,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
async function completeUpload(task) {
|
||||
try {
|
||||
logger.info(`文件接收完成,准备传输: ${ task.fileName }`)
|
||||
const safeTempFilePath = assertPathInside(sftpCacheDir, task.tempFilePath)
|
||||
|
||||
// 关闭写入流
|
||||
if (task.writeStream) {
|
||||
@@ -1209,7 +1245,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
const stats = fs.statSync(task.tempFilePath)
|
||||
const stats = fs.statSync(safeTempFilePath)
|
||||
if (stats.size !== task.fileSize) {
|
||||
throw new Error(`文件大小不匹配: 期望 ${ task.fileSize }, 实际 ${ stats.size }`)
|
||||
}
|
||||
@@ -1235,7 +1271,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
let lastSftpUpdateTime = sftpStartTime
|
||||
let lastSftpUploadedSize = 0
|
||||
|
||||
await sftpClient.fastPut(task.tempFilePath, task.targetPath, {
|
||||
await sftpClient.fastPut(safeTempFilePath, task.targetPath, {
|
||||
step: (transferredBytes) => {
|
||||
const now = Date.now()
|
||||
const sftpProgress = Math.min((transferredBytes / task.fileSize) * 100, 100)
|
||||
@@ -1283,9 +1319,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
})
|
||||
} finally {
|
||||
// 确保清理临时文件
|
||||
if (task.tempFilePath && fs.existsSync(task.tempFilePath)) {
|
||||
if (task.tempFilePath) {
|
||||
try {
|
||||
fs.unlinkSync(task.tempFilePath)
|
||||
removeCacheFileSync(task.tempFilePath)
|
||||
logger.info(`已清理临时文件: ${ task.tempFilePath }`)
|
||||
} catch (cleanupErr) {
|
||||
logger.warn('清理临时文件失败:', cleanupErr.message)
|
||||
@@ -1303,7 +1339,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
}
|
||||
|
||||
// 取消上传
|
||||
socket.on('upload_cancel', ({ taskId }) => {
|
||||
socket.on('upload_cancel', ({ taskId } = {}) => {
|
||||
const task = uploadTasks.get(taskId)
|
||||
if (task) {
|
||||
task.abortController.abort()
|
||||
@@ -1312,9 +1348,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
task.writeStream.destroy()
|
||||
}
|
||||
// 清理临时文件
|
||||
if (task.tempFilePath && fs.existsSync(task.tempFilePath)) {
|
||||
if (task.tempFilePath) {
|
||||
try {
|
||||
fs.unlinkSync(task.tempFilePath)
|
||||
removeCacheFileSync(task.tempFilePath)
|
||||
logger.info(`取消上传,已清理临时文件: ${ task.tempFilePath }`)
|
||||
} catch (cleanupErr) {
|
||||
logger.warn('清理临时文件失败:', cleanupErr.message)
|
||||
@@ -1376,9 +1412,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
task.writeStream.destroy()
|
||||
}
|
||||
// 清理临时文件
|
||||
if (task.tempFilePath && fs.existsSync(task.tempFilePath)) {
|
||||
if (task.tempFilePath) {
|
||||
try {
|
||||
fs.unlinkSync(task.tempFilePath)
|
||||
removeCacheFileSync(task.tempFilePath)
|
||||
logger.info(`连接断开,已清理临时文件: ${ task.tempFilePath }`)
|
||||
} catch (cleanupErr) {
|
||||
logger.warn('清理临时文件失败:', cleanupErr.message)
|
||||
@@ -1423,9 +1459,9 @@ const listenAction = (sftpClient, socket) => {
|
||||
task.writeStream.destroy()
|
||||
}
|
||||
// 清理临时文件
|
||||
if (task.tempFilePath && fs.existsSync(task.tempFilePath)) {
|
||||
if (task.tempFilePath) {
|
||||
try {
|
||||
fs.unlinkSync(task.tempFilePath)
|
||||
removeCacheFileSync(task.tempFilePath)
|
||||
logger.info(`清理超时任务临时文件: ${ task.tempFilePath }`)
|
||||
} catch (cleanupErr) {
|
||||
logger.warn('清理临时文件失败:', cleanupErr.message)
|
||||
@@ -1481,7 +1517,7 @@ const listenAction = (sftpClient, socket) => {
|
||||
// 错误处理函数
|
||||
const handleError = (err) => {
|
||||
cleanup()
|
||||
fs.unlink(localPath).catch(() => {})
|
||||
removeCacheFile(localPath).catch(() => {})
|
||||
reject(err)
|
||||
}
|
||||
|
||||
@@ -1528,12 +1564,16 @@ const listenAction = (sftpClient, socket) => {
|
||||
|
||||
try {
|
||||
readStream = sftpClient.createReadStream(remotePath)
|
||||
writeStream = fs.createWriteStream(localPath)
|
||||
const safeLocalPath = assertPathInside(sftpCacheDir, localPath)
|
||||
writeStream = fs.createWriteStream(safeLocalPath, {
|
||||
flags: 'wx',
|
||||
mode: 0o600
|
||||
})
|
||||
|
||||
readStream.on('data', (chunk) => {
|
||||
if (abortController.signal.aborted) {
|
||||
cleanup()
|
||||
fs.unlink(localPath).catch(() => {}) // 删除部分下载的文件
|
||||
removeCacheFile(localPath).catch(() => {}) // 删除部分下载的文件
|
||||
reject(new Error('下载已取消'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import path from 'node:path'
|
||||
|
||||
const TASK_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/
|
||||
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/
|
||||
|
||||
function assertPathInside(rootPath, candidatePath, { allowRoot = false } = {}) {
|
||||
const resolvedRoot = path.resolve(rootPath)
|
||||
const resolvedCandidate = path.resolve(candidatePath)
|
||||
const relativePath = path.relative(resolvedRoot, resolvedCandidate)
|
||||
const escapesRoot = relativePath === '..' ||
|
||||
relativePath.startsWith(`..${ path.sep }`) ||
|
||||
path.isAbsolute(relativePath)
|
||||
|
||||
if (escapesRoot || (!allowRoot && relativePath === '')) {
|
||||
throw new Error('缓存文件路径非法')
|
||||
}
|
||||
|
||||
return resolvedCandidate
|
||||
}
|
||||
|
||||
function resolvePathInside(rootPath, ...pathSegments) {
|
||||
const candidatePath = path.resolve(rootPath, ...pathSegments)
|
||||
return assertPathInside(rootPath, candidatePath)
|
||||
}
|
||||
|
||||
function validateTaskId(taskId) {
|
||||
if (typeof taskId !== 'string' || !TASK_ID_PATTERN.test(taskId)) {
|
||||
throw new Error('上传任务ID非法')
|
||||
}
|
||||
return taskId
|
||||
}
|
||||
|
||||
function validateRemoteFileName(fileName) {
|
||||
if (
|
||||
typeof fileName !== 'string' ||
|
||||
fileName.length === 0 ||
|
||||
fileName.length > 255 ||
|
||||
fileName === '.' ||
|
||||
fileName === '..' ||
|
||||
fileName.includes('/') ||
|
||||
fileName.includes('\\') ||
|
||||
CONTROL_CHARACTER_PATTERN.test(fileName)
|
||||
) {
|
||||
throw new Error('文件名非法')
|
||||
}
|
||||
return fileName
|
||||
}
|
||||
|
||||
export {
|
||||
assertPathInside,
|
||||
resolvePathInside,
|
||||
validateTaskId,
|
||||
validateRemoteFileName
|
||||
}
|
||||
+2
-1
@@ -10,7 +10,8 @@
|
||||
"start": "node ./index.js",
|
||||
"lint": "eslint . --ext .js,.vue",
|
||||
"lint:fix": "eslint . --ext .js,.jsx,.cjs,.mjs --fix",
|
||||
"test": "node test/test-rest-api-auth.js && node test/test-ws-comprehensive.js",
|
||||
"test": "node test/test-sftp-cache-path.js && node test/test-rest-api-auth.js && node test/test-ws-comprehensive.js",
|
||||
"test:security": "node test/test-sftp-cache-path.js",
|
||||
"test:api": "node test/test-rest-api-auth.js",
|
||||
"test:ws": "node test/test-ws-comprehensive.js",
|
||||
"test:mobile": "node test/test-mobile-crypto.js && node test/test-mobile-ssh-payload.js",
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import path from 'node:path'
|
||||
import {
|
||||
assertPathInside,
|
||||
resolvePathInside,
|
||||
validateRemoteFileName,
|
||||
validateTaskId
|
||||
} from '../app/utils/sftp-cache-path.js'
|
||||
|
||||
const cacheRoot = path.resolve('/tmp/easynode-sftp-cache-test')
|
||||
|
||||
assert.equal(validateTaskId('1720000000000-abc_123'), '1720000000000-abc_123')
|
||||
for (const taskId of ['', '../task', '../../../../utils/audit', 'task/name', 'task.name', 'a'.repeat(129)]) {
|
||||
assert.throws(() => validateTaskId(taskId), /上传任务ID非法/)
|
||||
}
|
||||
|
||||
assert.equal(validateRemoteFileName('report 2026.tar.gz'), 'report 2026.tar.gz')
|
||||
for (const fileName of ['', '.', '..', '../audit.js', 'folder/file.txt', 'folder\\file.txt', 'bad\nname']) {
|
||||
assert.throws(() => validateRemoteFileName(fileName), /文件名非法/)
|
||||
}
|
||||
|
||||
const safePath = resolvePathInside(cacheRoot, 'task-id', 'report.txt')
|
||||
assert.equal(safePath, path.join(cacheRoot, 'task-id', 'report.txt'))
|
||||
assert.equal(assertPathInside(cacheRoot, safePath), safePath)
|
||||
|
||||
for (const pathSegments of [
|
||||
['../outside.txt'],
|
||||
['temp_../../../../utils/audit.js'],
|
||||
['/etc/passwd']
|
||||
]) {
|
||||
assert.throws(() => resolvePathInside(cacheRoot, ...pathSegments), /缓存文件路径非法/)
|
||||
}
|
||||
|
||||
assert.throws(
|
||||
() => assertPathInside(cacheRoot, `${ cacheRoot }-other/file.txt`),
|
||||
/缓存文件路径非法/
|
||||
)
|
||||
assert.throws(() => assertPathInside(cacheRoot, cacheRoot), /缓存文件路径非法/)
|
||||
|
||||
console.log('SFTP 缓存路径安全测试通过')
|
||||
Reference in New Issue
Block a user