fix(main): パッケージ ID を展開先パスに使う前に検証する - #2465
Merged
Merged
Conversation
installPackageArchive は packageState.id をそのまま展開先フォルダ名に 使っている。unzip の getTargetPath は path.resolve で組み立てるだけで resolveInside を通しておらず、非アーカイブ経路の path.join も同じ。 同じ packageId を扱う openPackageFolder には 「packageId はリモート由来のため、データフォルダ外は拒否する」という isParent ガードが既に入っている。展開先だけがこの関門を通っていなかった。 既存の防御線に載せる。 unzip 基点を明示して resolveInside を通す packageInstall 非アーカイブ経路も同じ関門へ packages(api) tRPC 境界で id を isSafeRelativePath で検証する 境界での検証は files[].filename 等と同じ扱いで、AGENTS.md が 「形の細部はサービス層が二重に守る」と書いている設計に沿う。
pull Bot
pushed a commit
to ch2pw/apm
that referenced
this pull request
Aug 29, 2026
unzip は targetPath を消さずに overwrite: 'a' で展開する。overwrite は
アーカイブに在るものを上書きするだけなので、旧バージョンにしか無かった
ファイルは残り続ける。
targetPath は同じパッケージ(Data/package/{id})・同じアーカイブ名で
再利用されるため、残骸は次のインストールに持ち込まれる。install() が
ファイル単位でコピーする通常経路は列挙されたものしか拾わないが、
isDirectory のエントリと isProgram(展開結果を丸ごとコピー)は
subtree ごと持っていくので、削除されたはずのファイルが AviUtl 側へ
再配置される。
展開前に消す。targetPath は resolveInside を通しているので、消す対象は
必ず基点の内側にある(この PR を team-apm#2465 の上に積んでいるのはそのため。
関門を通っていない状態で remove を足すと、書き込みより危険な操作を
無検証のパスに対して行うことになる)。
代償として、展開に失敗すると前回の展開物も失われる。アーカイブは
Data/{core,package}/archive に残っているのでやり直せる。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
内容
installPackageArchiveはpackageState.idをそのまま展開先フォルダ名に使っています。unzipのgetTargetPathはpath.resolveで組み立てるだけで、resolveInsideを通していません。同じ
packageIdを扱うopenPackageFolderには既にガードがあります。展開先だけがこの関門を通っていませんでした。
変更
既存の防御線に載せます。AGENTS.md が「境界で守るのはパス・コマンド・URL に到達するフィールドだけで、形の細部はサービス層(
resolveInside/safeRemove/execFileSync)が二重に守る」と書いている設計に沿った形です。unzipresolveInsideを通すpackageInstallpackages(tRPC 境界)idをisSafeRelativePathで検証(files[].filename等と同じ扱い)unzipをファイル単位に作り替えずresolveInsideを挟むだけにしたのは、Data配下という基点がzipPathから決まるためです。基点の取り方(Data配下なら 1 階層上、それ以外はその場)は変えていません。テスト
unzip.test.tsに 2 件追加しました。userData/Data/package/archive/x.zip)を模し、基点の取り方が変わっていないことも同時に確認どちらも脱出先を後片付けの対象内に置いてあるので、テストが外部にファイルを残しません。
あわせて
これがマージされると、
unzipの展開先を展開前に掃除する変更(前回のインストールの残骸が新しいインストールに混入する件)も安全に足せるようになります。いまはtargetPathが関門を通っていないため、remove()を足すと「書ける」より危険な「消せる」を無検証のパスに対して行うことになるので保留していました。調査と文面の作成に Claude Code を使用しています。