应绕过直接操作windows acl,将权限判断收口到应用层,通过fileaccessservice.authorizeasync统一鉴权,结合claimsprincipal承载角色/资源/操作三元组,并用内存缓存高频权限、手动递归过滤目录枚举、物理路径沙箱隔离及日志脱敏来保障安全。

Windows ACL 和 .NET FileSecurity 不是 RBAC
直接在文件上设 FileSystemAccessRule 是硬编码权限,和角色无关。改个角色就得遍历所有文件重设 ACL,运维成本爆炸,且无法支持“项目经理可读项目目录下所有新文件”这类动态策略。
真正可行的路径是:**绕过直接操作 ACL,把权限判断收口到应用层**。文件系统只负责存储,权限逻辑由你的服务控制——比如所有文件访问必须经由 FileAccessService.AuthorizeAsync(userId, path, "read")。
- 用户登录后加载其角色(如
"editor"、"auditor"),缓存在内存或 Redis 中 - 每个文件/目录在数据库里存一条元记录,含
OwnerId、FolderId、PermissionPolicy(如"role:editor|read,write") - 拒绝直接暴露物理路径给前端;用短 ID(如
"f_8a2b")映射真实路径,防止路径遍历
如何用 ClaimsPrincipal 做运行时角色校验
.NET 的 ClaimsPrincipal 天然适合承载角色+资源+操作三元组。别只塞 ClaimTypes.Role,加自定义 claim:
new Claim("file:scope", "project-123"),
new Claim("file:action", "download")
这样 context.User.HasClaim(c => c.Type == "file:scope" && c.Value == folderId) 就能快速筛出上下文相关权限。
- 中间件里提前解析请求路径,提取目标文件夹 ID,注入到
HttpContext.Items["targetScope"] - 授权策略用
IAuthorizationHandler实现,查数据库匹配role+targetScope+requestedAction - 避免在每次
File.OpenRead()前都查 DB:对高频路径做内存缓存,键为$"{roleId}:{scopeId}:{action}"
Directory.EnumerateFiles() 返回结果必须过滤,不能只靠前置鉴权
即使用户没权限访问某个子目录,EnumerateFiles(root, "*", SearchOption.AllDirectories) 仍可能抛 UnauthorizedAccessException —— 这不是 bug,是 Windows API 行为。更糟的是,它会中断整个枚举,导致你漏掉有权限的其他路径。
- 永远用
SearchOption.TopDirectoryOnly+ 手动递归,每进一个子目录先调CanUserAccessAsync(path, "list") - 对
GetFiles()/GetDirectories()做 try-catch,捕获UnauthorizedAccessException和DirectoryNotFoundException,跳过而非崩溃 - 前端分页列表场景下,数据库查出有权限的路径集合再拼物理路径,比遍历文件系统更稳
物理路径隔离比加密更重要
别一上来就琢磨 AesCryptoServiceProvider 加密文件名。攻击者若已拿到服务器进程权限,密钥和内存里的解密逻辑一样暴露。优先做路径沙箱:
- 所有用户上传文件存到统一根目录(如
C:\app\uploads\),禁止用户指定任意路径 - 用哈希前缀分桶:
C:\app\uploads\ab\cd\ef\file.pdf,防止单目录文件过多 - Web 服务以非管理员身份运行,移除对
C:\、C:\Windows等系统路径的读写权限 - 如果真要加密,用 DPAPI(
ProtectedData.Protect())保护密钥,而不是自己管理 AES 密钥生命周期
最常被忽略的一点:日志里别打完整物理路径。log.Warn($"Failed to read {fullPath}") 可能泄露服务器结构。统一记为 "Failed to read file ID f_9d4x"。









