CRASH: FileUtils.copyDirectory 未检查目标是否落在 dest 树内 #11

Closed
opened 2026-11-09 14:23:58 +00:00 by layue13 · 1 comment
Owner

调研编号:扫描 2026-11-09 / C1

现象

FileUtils.copyDirectory 用 dest.resolve(src.relativize(file)) 后直接 Files.copy / createDirectories,没有对结果路径做 normalize + startsWith(dest) 守卫。同仓 Unzipper 已有该类防护,形成半边覆盖。若源树含 .. 分量,可能写出目标目录外。

危害类与优先级

  • 危害类:CRASH
  • 优先级:p2

证据(生效源码)

  • 载体:TDLM/HMCL + 分支 origin/main + pin/commit bdacd654734c3665995a8c779da34139b9bdf373
  • 位置:HMCLCore/.../FileUtils.java:308-331;对照正例 Unzipper.java:83-84
  • 关键原文:
// FileUtils.java:308-331
public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException {
    Files.walkFileTree(src, new SimpleFileVisitor<>() {
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
            if (!filePredicate.test(src.relativize(file).toString())) {
                return FileVisitResult.SKIP_SUBTREE;
            }

            Path destFile = dest.resolve(src.relativize(file).toString());
            Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING);
            return FileVisitResult.CONTINUE;
        }

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            if (!filePredicate.test(src.relativize(dir).toString())) {
                return FileVisitResult.SKIP_SUBTREE;
            }

            Path destDir = dest.resolve(src.relativize(dir).toString());
            Files.createDirectories(destDir);
            return FileVisitResult.CONTINUE;
        }
    });
}
// Unzipper.java:83-84(已有守卫,对照)
if (!destFile.startsWith(destDir)) {
    throw new IOException("Zip entry is trying to write outside of the destination directory: " + entry.getName());
}
  • 上游状态:未修

触发与复现

  • 触发条件:调用 copyDirectory 的路径(如 MultiMC 安装从整合包 libraries/ 拷到实例目录)遇到含 .. 的源相对路径;取决于 NIO 如何呈现相对路径。
  • 复现程度:静态推演
  • 包内相关性:模块 n/a/sides client/默认配置可达 是(安装整合包路径)

落点

  • 主仓:TDLM/HMCL → HMCLCore/.../FileUtils.java:308

范式

引证

  1. 生效源码 permalink — https://git.potatolab.uk/TDLM/HMCL/src/commit/bdacd654734c3665995a8c779da34139b9bdf373/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java#L308-L331
  2. 生效源码 permalink(对照正例)— https://git.potatolab.uk/TDLM/HMCL/src/commit/bdacd654734c3665995a8c779da34139b9bdf373/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java#L83-L84
  3. 范式 C1 — https://git.potatolab.uk/TDLM/mc-defect-patterns/raw/commit/12d2ebdc7a87175f936c2a67083be567e0e94301/crash/C1-服务端稳定性.md

来源

  • 线索类型:内部代码审查(无外部报障)
  • 可解析指针:FileUtils.java:308-331;commit bdacd654734c3665995a8c779da34139b9bdf373
  • 来源标签:origin/scan
**调研编号**:扫描 2026-11-09 / C1 ## 现象 `FileUtils.copyDirectory` 用 `dest.resolve(src.relativize(file))` 后直接 `Files.copy` / `createDirectories`,没有对结果路径做 `normalize` + `startsWith(dest)` 守卫。同仓 `Unzipper` 已有该类防护,形成半边覆盖。若源树含 `..` 分量,可能写出目标目录外。 ## 危害类与优先级 - 危害类:`CRASH` - 优先级:`p2` ## 证据(生效源码) - 载体:`TDLM/HMCL` + 分支 `origin/main` + pin/commit `bdacd654734c3665995a8c779da34139b9bdf373` - 位置:`HMCLCore/.../FileUtils.java:308-331`;对照正例 `Unzipper.java:83-84` - 关键原文: ```java // FileUtils.java:308-331 public static void copyDirectory(Path src, Path dest, Predicate<String> filePredicate) throws IOException { Files.walkFileTree(src, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { if (!filePredicate.test(src.relativize(file).toString())) { return FileVisitResult.SKIP_SUBTREE; } Path destFile = dest.resolve(src.relativize(file).toString()); Files.copy(file, destFile, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (!filePredicate.test(src.relativize(dir).toString())) { return FileVisitResult.SKIP_SUBTREE; } Path destDir = dest.resolve(src.relativize(dir).toString()); Files.createDirectories(destDir); return FileVisitResult.CONTINUE; } }); } ``` ```java // Unzipper.java:83-84(已有守卫,对照) if (!destFile.startsWith(destDir)) { throw new IOException("Zip entry is trying to write outside of the destination directory: " + entry.getName()); } ``` - 上游状态:未修 ## 触发与复现 - 触发条件:调用 `copyDirectory` 的路径(如 MultiMC 安装从整合包 `libraries/` 拷到实例目录)遇到含 `..` 的源相对路径;取决于 NIO 如何呈现相对路径。 - 复现程度:静态推演 - 包内相关性:模块 n/a/sides `client`/默认配置可达 是(安装整合包路径) ## 落点 - 主仓:`TDLM/HMCL` → `HMCLCore/.../FileUtils.java:308` ## 范式 - 命中范式:C1 服务端稳定性/应用稳定性(https://git.potatolab.uk/TDLM/mc-defect-patterns/raw/commit/12d2ebdc7a87175f936c2a67083be567e0e94301/crash/C1-服务端稳定性.md);与 Unzipper 对照亦见 G5 半边覆盖形状 ## 引证 1. 生效源码 permalink — https://git.potatolab.uk/TDLM/HMCL/src/commit/bdacd654734c3665995a8c779da34139b9bdf373/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/FileUtils.java#L308-L331 2. 生效源码 permalink(对照正例)— https://git.potatolab.uk/TDLM/HMCL/src/commit/bdacd654734c3665995a8c779da34139b9bdf373/HMCLCore/src/main/java/org/jackhuang/hmcl/util/io/Unzipper.java#L83-L84 3. 范式 C1 — https://git.potatolab.uk/TDLM/mc-defect-patterns/raw/commit/12d2ebdc7a87175f936c2a67083be567e0e94301/crash/C1-服务端稳定性.md ## 来源 - 线索类型:内部代码审查(无外部报障) - 可解析指针:`FileUtils.java:308-331`;commit `bdacd654734c3665995a8c779da34139b9bdf373` - 来源标签:`origin/scan`
layue13 added the
priority
p2
area/server
type
defect
origin/scan
labels 2026-11-09 14:23:58 +00:00
Author
Owner

Fixed on main via PR #14 — FileUtils.copyDirectory rejects paths that escape the destination root after normalize.

Merge: #14
Commit: c4240a24bf

Fixed on `main` via PR #14 — `FileUtils.copyDirectory` rejects paths that escape the destination root after normalize. Merge: https://git.nest.potatolab.uk:8443/TDLM/HMCL/pulls/14 Commit: c4240a24bf7635a26d6e45721c77b33d94bcb252
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: TDLM/HMCL#11