Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
B
bit-pm
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
燕伟桐
bit-pm
Commits
a985a5aa
Unverified
提交
a985a5aa
authored
6月 25, 2025
作者:
Will Chen
提交者:
GitHub
6月 25, 2025
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Ensure fs changes in response processor are within app dir (#496)
上级
2ea9500f
显示空白字符变更
内嵌
并排
正在显示
3 个修改的文件
包含
294 行增加
和
6 行删除
+294
-6
path_utils.test.ts
src/__tests__/path_utils.test.ts
+227
-0
response_processor.ts
src/ipc/processors/response_processor.ts
+7
-6
path_utils.ts
src/ipc/utils/path_utils.ts
+60
-0
没有找到文件。
src/__tests__/path_utils.test.ts
0 → 100644
浏览文件 @
a985a5aa
import
{
safeJoin
}
from
"@/ipc/utils/path_utils"
;
import
{
describe
,
it
,
expect
}
from
"vitest"
;
import
path
from
"node:path"
;
import
os
from
"node:os"
;
describe
(
"safeJoin"
,
()
=>
{
const
testBaseDir
=
"/app/workspace"
;
const
testBaseDirWindows
=
"C:
\\
app
\\
workspace"
;
describe
(
"safe paths"
,
()
=>
{
it
(
"should join simple relative paths"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"src"
,
"components"
,
"Button.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src"
,
"components"
,
"Button.tsx"
),
);
});
it
(
"should handle single file names"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"package.json"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"package.json"
));
});
it
(
"should handle nested directories"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"src/pages/home/index.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src/pages/home/index.tsx"
));
});
it
(
"should handle paths with dots in filename"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"config.test.js"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"config.test.js"
));
});
it
(
"should handle empty path segments"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
""
,
"src"
,
""
,
"file.ts"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
""
,
"src"
,
""
,
"file.ts"
));
});
it
(
"should handle multiple path segments"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"a"
,
"b"
,
"c"
,
"d"
,
"file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"a"
,
"b"
,
"c"
,
"d"
,
"file.txt"
),
);
});
it
(
"should work with actual temp directory"
,
()
=>
{
const
tempDir
=
os
.
tmpdir
();
const
result
=
safeJoin
(
tempDir
,
"test"
,
"file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
tempDir
,
"test"
,
"file.txt"
));
});
it
(
"should handle Windows-style relative paths with backslashes"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"src
\\
components
\\
Button.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src
\\
components
\\
Button.tsx"
),
);
});
it
(
"should handle mixed forward/backslashes in relative paths"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"src/components
\\
ui/button.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src/components
\\
ui/button.tsx"
),
);
});
it
(
"should handle Windows-style nested directories"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"pages
\\
home
\\
components
\\
index.tsx"
,
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"pages
\\
home
\\
components
\\
index.tsx"
),
);
});
it
(
"should handle relative paths starting with dot and backslash"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
".
\\
src
\\
file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
".
\\
src
\\
file.txt"
));
});
});
describe
(
"unsafe paths - directory traversal"
,
()
=>
{
it
(
"should throw on simple parent directory traversal"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"../outside.txt"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should throw on multiple parent directory traversals"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"../../etc/passwd"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should throw on complex traversal paths"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"src/../../../etc/passwd"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should throw on mixed traversal with valid components"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"src"
,
"components"
,
".."
,
".."
,
".."
,
"outside.txt"
,
),
).
toThrow
(
/would escape the base directory/
);
});
it
(
"should throw on absolute Unix paths"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"/etc/passwd"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should throw on absolute Windows paths"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"C:
\\
Windows
\\
System32
\\
config"
),
).
toThrow
(
/would escape the base directory/
);
});
it
(
"should throw on Windows UNC paths"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"
\\\\
server
\\
share
\\
file.txt"
),
).
toThrow
(
/would escape the base directory/
);
});
it
(
"should throw on home directory shortcuts"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"~/secrets.txt"
)).
toThrow
(
/would escape the base directory/
,
);
});
});
describe
(
"edge cases"
,
()
=>
{
it
(
"should handle Windows-style base paths"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDirWindows
,
"src"
,
"file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDirWindows
,
"src"
,
"file.txt"
));
});
it
(
"should throw on Windows traversal from Unix base"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"..
\\
..
\\
file.txt"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should handle current directory references safely"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"./src/file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"./src/file.txt"
));
});
it
(
"should handle nested current directory references"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"src/./components/./Button.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src/./components/./Button.tsx"
),
);
});
it
(
"should throw when current dir plus traversal escapes"
,
()
=>
{
expect
(()
=>
safeJoin
(
testBaseDir
,
"./../../outside.txt"
)).
toThrow
(
/would escape the base directory/
,
);
});
it
(
"should handle very long paths safely"
,
()
=>
{
const
longPath
=
Array
(
50
).
fill
(
"subdir"
).
join
(
"/"
)
+
"/file.txt"
;
const
result
=
safeJoin
(
testBaseDir
,
longPath
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
longPath
));
});
it
(
"should allow Windows-style paths that look like drive letters but aren't"
,
()
=>
{
// These look like they could be problematic but are actually safe relative paths
const
result1
=
safeJoin
(
testBaseDir
,
"C_drive
\\
file.txt"
);
expect
(
result1
).
toBe
(
path
.
join
(
testBaseDir
,
"C_drive
\\
file.txt"
));
const
result2
=
safeJoin
(
testBaseDir
,
"src
\\
C-file.txt"
);
expect
(
result2
).
toBe
(
path
.
join
(
testBaseDir
,
"src
\\
C-file.txt"
));
});
it
(
"should handle Windows paths with multiple backslashes (not UNC)"
,
()
=>
{
// Single backslashes in the middle are fine - it's only \\ at the start that's UNC
const
result
=
safeJoin
(
testBaseDir
,
"src
\\\\
components
\\\\
Button.tsx"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"src
\\\\
components
\\\\
Button.tsx"
),
);
});
it
(
"should provide descriptive error messages"
,
()
=>
{
expect
(()
=>
safeJoin
(
"/base"
,
"../outside.txt"
)).
toThrow
(
'Unsafe path: joining "../outside.txt" with base "/base" would escape the base directory'
,
);
});
it
(
"should provide descriptive error for multiple segments"
,
()
=>
{
expect
(()
=>
safeJoin
(
"/base"
,
"src"
,
".."
,
".."
,
"outside.txt"
)).
toThrow
(
'Unsafe path: joining "src, .., .., outside.txt" with base "/base" would escape the base directory'
,
);
});
});
describe
(
"boundary conditions"
,
()
=>
{
it
(
"should allow paths at the exact boundary"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"."
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"."
));
});
it
(
"should handle paths that approach but don't cross boundary"
,
()
=>
{
const
result
=
safeJoin
(
testBaseDir
,
"deep/nested/../file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
testBaseDir
,
"deep/nested/../file.txt"
));
});
it
(
"should handle root directory as base"
,
()
=>
{
const
result
=
safeJoin
(
"/"
,
"tmp/file.txt"
);
expect
(
result
).
toBe
(
path
.
join
(
"/"
,
"tmp/file.txt"
));
});
it
(
"should throw when trying to escape root"
,
()
=>
{
expect
(()
=>
safeJoin
(
"/tmp"
,
"../etc/passwd"
)).
toThrow
(
/would escape the base directory/
,
);
});
});
});
src/ipc/processors/response_processor.ts
浏览文件 @
a985a5aa
...
@@ -5,6 +5,7 @@ import fs from "node:fs";
...
@@ -5,6 +5,7 @@ import fs from "node:fs";
import
{
getDyadAppPath
}
from
"../../paths/paths"
;
import
{
getDyadAppPath
}
from
"../../paths/paths"
;
import
path
from
"node:path"
;
import
path
from
"node:path"
;
import
git
from
"isomorphic-git"
;
import
git
from
"isomorphic-git"
;
import
{
safeJoin
}
from
"../utils/path_utils"
;
import
log
from
"electron-log"
;
import
log
from
"electron-log"
;
import
{
executeAddDependency
}
from
"./executeAddDependency"
;
import
{
executeAddDependency
}
from
"./executeAddDependency"
;
...
@@ -296,11 +297,11 @@ export async function processFullResponseActions(
...
@@ -296,11 +297,11 @@ export async function processFullResponseActions(
}
}
writtenFiles
.
push
(
"package.json"
);
writtenFiles
.
push
(
"package.json"
);
const
pnpmFilename
=
"pnpm-lock.yaml"
;
const
pnpmFilename
=
"pnpm-lock.yaml"
;
if
(
fs
.
existsSync
(
path
.
j
oin
(
appPath
,
pnpmFilename
)))
{
if
(
fs
.
existsSync
(
safeJ
oin
(
appPath
,
pnpmFilename
)))
{
writtenFiles
.
push
(
pnpmFilename
);
writtenFiles
.
push
(
pnpmFilename
);
}
}
const
packageLockFilename
=
"package-lock.json"
;
const
packageLockFilename
=
"package-lock.json"
;
if
(
fs
.
existsSync
(
path
.
j
oin
(
appPath
,
packageLockFilename
)))
{
if
(
fs
.
existsSync
(
safeJ
oin
(
appPath
,
packageLockFilename
)))
{
writtenFiles
.
push
(
packageLockFilename
);
writtenFiles
.
push
(
packageLockFilename
);
}
}
}
}
...
@@ -319,7 +320,7 @@ export async function processFullResponseActions(
...
@@ -319,7 +320,7 @@ export async function processFullResponseActions(
// Process all file deletions
// Process all file deletions
for
(
const
filePath
of
dyadDeletePaths
)
{
for
(
const
filePath
of
dyadDeletePaths
)
{
const
fullFilePath
=
path
.
j
oin
(
appPath
,
filePath
);
const
fullFilePath
=
safeJ
oin
(
appPath
,
filePath
);
// Delete the file if it exists
// Delete the file if it exists
if
(
fs
.
existsSync
(
fullFilePath
))
{
if
(
fs
.
existsSync
(
fullFilePath
))
{
...
@@ -362,8 +363,8 @@ export async function processFullResponseActions(
...
@@ -362,8 +363,8 @@ export async function processFullResponseActions(
// Process all file renames
// Process all file renames
for
(
const
tag
of
dyadRenameTags
)
{
for
(
const
tag
of
dyadRenameTags
)
{
const
fromPath
=
path
.
j
oin
(
appPath
,
tag
.
from
);
const
fromPath
=
safeJ
oin
(
appPath
,
tag
.
from
);
const
toPath
=
path
.
j
oin
(
appPath
,
tag
.
to
);
const
toPath
=
safeJ
oin
(
appPath
,
tag
.
to
);
// Ensure target directory exists
// Ensure target directory exists
const
dirPath
=
path
.
dirname
(
toPath
);
const
dirPath
=
path
.
dirname
(
toPath
);
...
@@ -427,7 +428,7 @@ export async function processFullResponseActions(
...
@@ -427,7 +428,7 @@ export async function processFullResponseActions(
for
(
const
tag
of
dyadWriteTags
)
{
for
(
const
tag
of
dyadWriteTags
)
{
const
filePath
=
tag
.
path
;
const
filePath
=
tag
.
path
;
const
content
=
tag
.
content
;
const
content
=
tag
.
content
;
const
fullFilePath
=
path
.
j
oin
(
appPath
,
filePath
);
const
fullFilePath
=
safeJ
oin
(
appPath
,
filePath
);
// Ensure directory exists
// Ensure directory exists
const
dirPath
=
path
.
dirname
(
fullFilePath
);
const
dirPath
=
path
.
dirname
(
fullFilePath
);
...
...
src/ipc/utils/path_utils.ts
0 → 100644
浏览文件 @
a985a5aa
import
path
from
"node:path"
;
/**
* Safely joins paths while ensuring the result stays within the base directory.
* This prevents directory traversal attacks where malicious paths like "../../etc/passwd"
* could be used to access files outside the intended directory.
*
* @param basePath The base directory that should contain the result
* @param ...paths Path segments to join with the base path
* @returns The joined path if it's within the base directory
* @throws Error if the resulting path would be outside the base directory
*/
export
function
safeJoin
(
basePath
:
string
,
...
paths
:
string
[]):
string
{
// Check if any of the path segments are absolute paths (which would be unsafe)
for
(
const
pathSegment
of
paths
)
{
if
(
path
.
isAbsolute
(
pathSegment
))
{
throw
new
Error
(
`Unsafe path: joining "
${
paths
.
join
(
", "
)}
" with base "
${
basePath
}
" would escape the base directory`
,
);
}
// Also check for home directory shortcuts which are effectively absolute
if
(
pathSegment
.
startsWith
(
"~/"
))
{
throw
new
Error
(
`Unsafe path: joining "
${
paths
.
join
(
", "
)}
" with base "
${
basePath
}
" would escape the base directory`
,
);
}
// Check for Windows-style absolute paths (C:\, D:\, etc.)
if
(
/^
[
A-Za-z
]
:
[/\\]
/
.
test
(
pathSegment
))
{
throw
new
Error
(
`Unsafe path: joining "
${
paths
.
join
(
", "
)}
" with base "
${
basePath
}
" would escape the base directory`
,
);
}
// Check for UNC paths (\\server\share)
if
(
pathSegment
.
startsWith
(
"
\\\
\"
)) {
throw new Error(
`Unsafe path: joining "
$
{
paths
.
join
(
", "
)}
" with base "
$
{
basePath
}
" would escape the base directory`,
);
}
}
// Join all the paths
const joinedPath = path.join(basePath, ...paths);
// Resolve both paths to absolute paths to handle any "
..
" components
const resolvedBasePath = path.resolve(basePath);
const resolvedJoinedPath = path.resolve(joinedPath);
// Check if the resolved joined path starts with the base path
// Use path.relative to ensure we're doing a proper path comparison
const relativePath = path.relative(resolvedBasePath, resolvedJoinedPath);
// If relativePath starts with "
..
" or is absolute, then resolvedJoinedPath is outside basePath
if (relativePath.startsWith("
..
") || path.isAbsolute(relativePath)) {
throw new Error(
`Unsafe path: joining "
$
{
paths
.
join
(
", "
)}
" with base "
$
{
basePath
}
" would escape the base directory`,
);
}
return joinedPath;
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论