Skip to content

Commit 22fe270

Browse files
committed
fix: 安全审查修复 - 路径验证、密码编码、Zip Slip防护
1 parent ac9a801 commit 22fe270

4 files changed

Lines changed: 56 additions & 26 deletions

File tree

src-tauri/locales/zh-cn.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,9 @@
161161
"filter_rules": "过滤规则",
162162
"filter_rules_placeholder": "每行一条规则,例如:\n+ *.jpg\n+ *.png\n- *.tmp\n- *.log",
163163
"filter_rules_help": "rclone 过滤规则:+ 表示包含,- 表示排除。支持通配符 * 和 ?。留空表示不过滤。",
164-
"resync": "重新同步",
165-
"force_resync": "强制重新同步",
166-
"force_resync_tip": "使用 --resync 标志强制完全重新同步。首次设置或同步状态损坏时使用。",
167-
"resync": "重新同步",
168-
"force_resync": "强制重新同步",
169-
"force_resync_tip": "使用 --resync 标志强制完全重新同步。首次设置或同步状态损坏时使用。",
164+
"resync": "重新同步",
165+
"force_resync": "强制重新同步",
166+
"force_resync_tip": "使用 --resync 标志强制完全重新同步。首次设置或同步状态损坏时使用。",
170167
"add_storage": "添加存储",
171168
"add_mount": "添加挂载",
172169
"add_task": "添加任务",

src-tauri/locales/zh-hant.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,10 @@
674674
"unable_to_obtain_transmission_speed": "當前可能無法獲取具體傳送速率,但傳輸仍在進行。",
675675
"temp_path": "臨時路徑",
676676
"cache_path": "緩存路徑",
677+
"clear_cache": "清理緩存",
678+
"clear_cache_confirm": "確定要清理所有緩存文件嗎?這將刪除所有臨時緩存數據。",
679+
"cache_cleared": "緩存已清理",
680+
"no_cache_to_clear": "沒有需要清理的緩存",
677681
"please_select_cache_dir": "請選擇緩存路徑",
678682
"select": "選擇",
679683
"ask_restartself": "是否重啓軟件?",

src-tauri/src/fs.rs

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -129,17 +129,21 @@ fn app_data_dir(app: &tauri::AppHandle<Runtime>) -> anyhow::Result<PathBuf> {
129129

130130
#[tauri::command]
131131
pub fn fs_exist_dir(app: tauri::AppHandle<Runtime>, path: &str) -> anyhow_tauri::TAResult<bool> {
132-
let path = resolve_path(&app, path)?;
133-
let exists = std::fs::metadata(path)
132+
let resolved_path = resolve_path(&app, path)?;
133+
// 安全:验证路径在允许目录内
134+
let validated_path = validate_path_in_allowed_dir(&app, &resolved_path)?;
135+
let exists = std::fs::metadata(validated_path)
134136
.map_err(anyhow::Error::from)?
135137
.is_dir();
136138
Ok(exists)
137139
}
138140

139141
#[tauri::command]
140142
pub fn fs_make_dir(app: tauri::AppHandle<Runtime>, path: &str) -> anyhow_tauri::TAResult<()> {
141-
let path = resolve_path(&app, path)?;
142-
std::fs::create_dir_all(path).map_err(anyhow::Error::from)?;
143+
let resolved_path = resolve_path(&app, path)?;
144+
// 安全:验证路径在允许目录内
145+
let validated_path = validate_path_in_allowed_dir(&app, &resolved_path)?;
146+
std::fs::create_dir_all(validated_path).map_err(anyhow::Error::from)?;
143147
Ok(())
144148
}
145149

@@ -344,6 +348,10 @@ pub fn export_config(
344348
}
345349

346350
let out = resolve_tilde(app, out_path)?;
351+
352+
// 安全:验证输出路径在允许目录内
353+
validate_path_in_allowed_dir(app, &out)?;
354+
347355
if let Some(parent) = out.parent() {
348356
if !parent.as_os_str().is_empty() {
349357
fs::create_dir_all(parent).map_err(anyhow::Error::from)?;
@@ -465,25 +473,29 @@ pub fn import_config(
465473
let target_path = temp_dir.join(&entry_name);
466474

467475
// 安全:验证目标路径在临时目录内(防止 Zip Slip)
468-
let temp_dir_canonical = temp_dir.canonicalize().unwrap_or_else(|_| temp_dir.clone());
469-
// 对于新创建的文件,需要检查父目录
470-
if let Some(parent) = target_path.parent() {
471-
if parent.exists() {
472-
let parent_canonical = parent.canonicalize().unwrap_or_else(|_| parent.to_path_buf());
473-
if !parent_canonical.starts_with(&temp_dir_canonical) {
474-
return Err(anyhow::anyhow!(
475-
"安全警告:ZIP 条目路径 '{}' 试图逃逸目标目录",
476-
entry_name
477-
));
478-
}
479-
}
476+
// 首先检查路径字符串是否包含可疑字符
477+
if entry_name.contains("..") || entry_name.contains('\\') || entry_name.starts_with('/') {
478+
return Err(anyhow::anyhow!(
479+
"安全警告:ZIP 文件包含可疑路径 '{}'",
480+
entry_name
481+
));
480482
}
481483

482484
// 确保父目录存在
483485
if let Some(parent) = target_path.parent() {
484486
fs::create_dir_all(parent)?;
485487
}
486488

489+
// 验证目标路径在临时目录内
490+
let temp_dir_canonical = temp_dir.canonicalize().unwrap_or_else(|_| temp_dir.clone());
491+
let target_canonical = target_path.canonicalize().unwrap_or_else(|_| target_path.clone());
492+
if !target_canonical.starts_with(&temp_dir_canonical) {
493+
return Err(anyhow::anyhow!(
494+
"安全警告:ZIP 条目路径 '{}' 试图逃逸目标目录",
495+
entry_name
496+
));
497+
}
498+
487499
// 如果是目录,创建它
488500
if entry.is_dir() {
489501
fs::create_dir_all(&target_path)?;

src/utils/passwordEncoding.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,26 @@
88

99
const ENCODING_PREFIX = 'nmenc:'
1010

11+
/**
12+
* 生成机器特定的密钥
13+
* 使用hostname和应用路径组合生成密钥,使配置文件在不同机器上不可移植
14+
*/
15+
function getMachineKey(): string {
16+
try {
17+
// 在浏览器环境中,使用 navigator 信息
18+
const hostname = window.location.hostname || 'localhost'
19+
const origin = window.location.origin || 'file://'
20+
// 组合生成机器特定密钥
21+
return `NetMount_${hostname}_${origin}_2024!`
22+
} catch {
23+
// 回退到固定密钥(向后兼容)
24+
return 'NetMount2024!'
25+
}
26+
}
27+
1128
/**
1229
* 简单的 XOR 编码函数
13-
* 使用固定的密钥对密码进行 XOR 编码,然后 base64 编码
30+
* 使用机器特定的密钥对密码进行 XOR 编码,然后 base64 编码
1431
*/
1532
function xorEncode(input: string, key: string): string {
1633
let result = ''
@@ -34,8 +51,8 @@ export function encodePassword(plainPassword: string): string {
3451
return plainPassword
3552
}
3653

37-
// 使用 XOR 编码 + base64
38-
const key = 'NetMount2024!' // 固定密钥
54+
// 使用机器特定密钥进行 XOR 编码 + base64
55+
const key = getMachineKey()
3956
const encoded = xorEncode(plainPassword, key)
4057
const base64 = btoa(unescape(encodeURIComponent(encoded)))
4158

@@ -58,7 +75,7 @@ export function decodePassword(encodedPassword: string): string {
5875
try {
5976
const base64 = encodedPassword.slice(ENCODING_PREFIX.length)
6077
const encoded = decodeURIComponent(escape(atob(base64)))
61-
const key = 'NetMount2024!'
78+
const key = getMachineKey()
6279
return xorEncode(encoded, key) // XOR 编码和解码是同一个操作
6380
} catch {
6481
// 解码失败,返回原值(可能是旧格式)

0 commit comments

Comments
 (0)