Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 70 additions & 31 deletions apps/shellsmith-mcp/src/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1434,6 +1434,7 @@ fn read_report(server: &Arc<Server>, args: Value) -> Result<ToolResult> {
if path.extension().and_then(|value| value.to_str()) != Some("json") {
anyhow::bail!("保护报告必须是 JSON 文件");
}
ensure_regular_file(&path, "保护报告")?;
let metadata = fs::metadata(&path)?;
if metadata.len() > 64 * 1024 * 1024 {
anyhow::bail!("保护报告超过 64 MiB");
Expand Down Expand Up @@ -1726,6 +1727,17 @@ fn absolute_entry_path(value: &str) -> Result<PathBuf> {
}
}

fn ensure_regular_file(path: &Path, label: &str) -> Result<()> {
let metadata =
fs::metadata(path).with_context(|| format!("读取{label}元数据失败: {}", path.display()))?;
anyhow::ensure!(
metadata.is_file(),
"{label}不是普通文件: {}",
path.display()
);
Ok(())
}

const MAX_IOS_BUNDLE_ENTRIES: usize = 50_000;
const MAX_IOS_BUNDLE_DEPTH: usize = 32;

Expand All @@ -1748,6 +1760,32 @@ fn validate_ios_bundle(server: &Arc<Server>, root: &Path) -> Result<()> {
continue;
}
visited.insert(directory.clone(), depth);
// 使用 CLI 的读取路径,让文件系统匹配大小写,同时识别必需文件的失效链接。
for name in ["project.pbxproj", "contents.xcworkspacedata"] {
let path = directory.join(name);
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(error).with_context(|| {
format!("读取 iOS 工程必需文件元数据失败: {}", path.display())
});
}
};
if metadata.file_type().is_symlink() {
let target = path.canonicalize().with_context(|| {
format!("iOS 工程必需文件符号链接无法解析: {}", path.display())
})?;
server.ensure_allowed(&target, "iOS 工程内部路径")?;
ensure_regular_file(&target, "iOS 工程必需文件")?;
} else {
anyhow::ensure!(
metadata.is_file(),
"iOS 工程必需文件不是普通文件: {}",
path.display()
);
}
}
let children = fs::read_dir(&directory)
.with_context(|| format!("读取 iOS 工程目录失败: {}", directory.display()))?;
for child in children {
Expand All @@ -1759,29 +1797,18 @@ fn validate_ios_bundle(server: &Arc<Server>, root: &Path) -> Result<()> {
root.display()
);
let path = child.path();
let metadata = fs::symlink_metadata(&path)
let file_type = child
.file_type()
.with_context(|| format!("读取 iOS 工程路径失败: {}", path.display()))?;
if metadata.file_type().is_symlink() {
let required = matches!(
path.file_name().and_then(|value| value.to_str()),
Some("project.pbxproj" | "contents.xcworkspacedata")
);
match path.canonicalize() {
Ok(target) => {
server.ensure_allowed(&target, "iOS 工程内部路径")?;
if target.is_dir() {
pending.push((target, depth + 1));
}
}
Err(error) if required => {
return Err(anyhow::Error::new(error).context(format!(
"iOS 工程必需文件符号链接无法解析: {}",
path.display()
)));
}
Err(_) => {}
if file_type.is_symlink() {
let Ok(target) = path.canonicalize() else {
continue;
};
server.ensure_allowed(&target, "iOS 工程内部路径")?;
if target.is_dir() {
pending.push((target, depth + 1));
}
} else if metadata.is_dir() {
} else if file_type.is_dir() {
pending.push((path, depth + 1));
}
}
Expand Down Expand Up @@ -1903,10 +1930,13 @@ fn validate_ios_cocoapods_paths(server: &Arc<Server>, project: &Path) -> Result<
let project_dir = if internal_workspace { parent } else { project };
let pbx_path = project_dir.join("project.pbxproj");
// 读取前先校验实际路径;内部 workspace 的父工程 pbx 不在 workspace bundle 的扫描范围内。
if pbx_path.exists() {
server.resolve_input(&pbx_path.to_string_lossy(), "iOS project.pbxproj")?;
}
let pbx = fs::read_to_string(&pbx_path).unwrap_or_default();
let pbx = if pbx_path.exists() {
let pbx_path = server.resolve_input(&pbx_path.to_string_lossy(), "iOS project.pbxproj")?;
ensure_regular_file(&pbx_path, "iOS project.pbxproj")?;
fs::read_to_string(&pbx_path).unwrap_or_default()
} else {
String::new()
};
let uses_pods = root.join("Podfile").is_file()
|| root.join("podfile").is_file()
|| root.join("Podfile.lock").is_file()
Expand All @@ -1916,25 +1946,34 @@ fn validate_ios_cocoapods_paths(server: &Arc<Server>, project: &Path) -> Result<
return Ok(());
}

for (path, label) in [
(root.join("Podfile"), "CocoaPods Podfile"),
(root.join("podfile"), "CocoaPods podfile"),
(root.join("Podfile.lock"), "CocoaPods Podfile.lock"),
(root.join("Pods/Manifest.lock"), "CocoaPods Manifest.lock"),
for (path, label, regular_required) in [
(root.join("Podfile"), "CocoaPods Podfile", false),
(root.join("podfile"), "CocoaPods podfile", false),
(root.join("Podfile.lock"), "CocoaPods Podfile.lock", true),
(
root.join("Pods/Manifest.lock"),
"CocoaPods Manifest.lock",
true,
),
(
root.join("Pods/Pods.xcodeproj/project.pbxproj"),
"CocoaPods Pods 工程",
false,
),
] {
if path.exists() {
server.resolve_input(&path.to_string_lossy(), label)?;
let resolved = server.resolve_input(&path.to_string_lossy(), label)?;
if regular_required {
ensure_regular_file(&resolved, label)?;
}
}
}
Ok(())
}

/// iOS 配置中的工程、entrypoint 和 Swift Confidential 路径也必须受允许目录约束。
fn validate_ios_config_paths(server: &Arc<Server>, config: &Path, dry_run: bool) -> Result<()> {
ensure_regular_file(config, "iOS 配置")?;
let content = fs::read_to_string(config)
.with_context(|| format!("读取 iOS 配置失败:{}", config.display()))?;
let value: toml::Value = toml::from_str(&content).context("解析 iOS 配置失败")?;
Expand Down
Loading
Loading