C#文件系统沙箱 C#如何限制代码只能在指定目录内操作文件

1次阅读

Directory.getcurrentdirectory() 不能当沙箱用,因其进程级且易被..、绝对路径、unc、符号链接绕过;需结合路径规范化校验(path.getfullpath+根目录白名单)与系统级jobobject隔离。

C#文件系统沙箱 C#如何限制代码只能在指定目录内操作文件

为什么 Directory.GetCurrentDirectory() 不能当沙箱用

很多开发者以为把工作目录切到某个子目录,再用相对路径操作文件,就能限制访问范围。实际上 Directory.SetCurrentDirectory() 是进程级的,且 ..、绝对路径、UNC 路径、符号链接(如果启用)都能绕过——File.ReadAllText("../../../etc/passwd")windows 上虽不生效,但在 .NET 6+ 启用 System.IO.EnableUnsafeIO 或跨平台部署时风险真实存在。

真正可控的方式是:所有路径必须显式校验,且禁止解析为指定根目录之外的物理路径。

Path.GetFullPath() + 根目录白名单做路径规范化校验

核心逻辑是:把用户传入的任意路径(相对/绝对/含 ..)转成绝对路径,再判断是否以沙箱根目录为前缀,且不跨越边界(避免软链接逃逸需额外处理)。

  • String sandboxRoot = Path.GetFullPath(@"C:sandbox"); —— 必须用 GetFullPath 消除歧义
  • 对每个待操作路径调用 Path.GetFullPath(userInput, sandboxRoot),第二个参数作为基准目录处理相对路径
  • 检查结果是否以 sandboxRoot + Path.DirectorySeparatorChar 开头(注意末尾斜杠!否则 C:sandbox2 会被误判为合法)
  • 拒绝含 :://、UNC(servershare)或空驱动器号的路径(如 "D:file.txt" 是相对当前 D 盘目录,不安全)

示例校验函数:

bool IsInSandbox(string userInput, string sandboxRoot) {     try     {         var fullPath = Path.GetFullPath(userInput, sandboxRoot);         // 确保 sandboxRoot 以分隔符结尾,避免前缀误匹配         var normalizedRoot = sandboxRoot.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)                                    + Path.DirectorySeparatorChar;         return fullPath.StartsWith(normalizedRoot, StringComparison.Ordinal)              && !fullPath.Contains(Path.VolumeSeparatorChar); // 排除 "C:xxx" 这类盘符相对路径     }     catch     {         return false;     } }

拦截 FileStreamFile 类的底层调用

仅靠路径校验不够——用户可能直接 new FileStream(@"C:windowssystem32hosts", FileMode.Open) 绕过你的封装。需要在关键 I/O 入口统一拦截:

  • 不直接暴露 FileDirectory 静态方法,封装成 SandboxFile.Read... 等方法,内部强制走校验
  • 若需支持 FileStream 构造,重载时要求传入预校验后的 string safePath,禁止接受原始字符串
  • 对第三方库调用(如 XmlSerializerJsonSerializer 的文件读写),必须传入 Stream 实例而非路径——自己用 File.OpenRead(safePath) 创建后传入
  • 警惕 Assembly.LoadFile()AppDomain.CreateDomain() 等可能触发文件加载的 API,它们不走你封装的路径校验

Windows 上的硬隔离:用 JobObject 限制进程文件句柄

纯托管层校验可被反射、动态代码或 P/Invoke 绕过。若需更强保障(如运行不受信脚本),需结合系统级隔离:

  • CreateJobObject + SetInformationJobObject 设置 JobObjectBasicLimitInformation,开启 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
  • 关键的是调用 AssignProcessToJobObject 把子进程(如编译后的 Roslyn 脚本进程)绑定到 Job 中
  • 配合 JobObjectSecurityLimitInformation 禁用 JOB_OBJECT_SECURITY_RESTRICTED_TOKEN,并设置最小权限令牌(去掉 SeBackupPrivilege 等)
  • 注意:.NET 进程默认无法创建 JobObject,需以 SeCreateJobPrivilege 权限运行,且 Job 对象本身需设 DACL 防止被外部进程关联

这层机制不替代路径校验,而是兜底——即使代码绕过 C# 层校验,系统也会在打开越界文件时返回 ERROR_ACCESS_DENIED

路径校验容易漏掉符号链接和驱动器相对路径,JobObject 配置稍有不慎会导致子进程立即退出;两者必须配合使用,且沙箱根目录本身权限也要设为仅当前用户可遍历(DirectorySecurity 控制)。

text=ZqhQzanResources