説明
Zip Slipは、アーカイブの entryName や fileName を検証せずに出力パスとして使うことで発生します。攻撃者が ../ や C:\、/tmp などの絶対パスを使うと、意図した展開先の外へ書き込まれる可能性があります。書き込み先とアプリケーションの権限によっては、実行ファイルの配置や設定の上書きにつながり、サービスに影響します。
想定される影響
- アプリケーションの権限でのファイルの作成や上書き
- Webルートや起動時のスクリプトなど、展開したファイルが後で実行される場合のコード実行
- 設定、バイナリ、ログの上書きによる障害
- アプリケーションのデータやログの改ざん
対処方法
- エントリのパスを正規化し、絶対パスや親ディレクトリの要素を拒否してください。
path.resolveで求めた最終パスが展開先の内部にあるか、path.relativeで確認してください。 C:\などのWindowsドライブ接頭辞を拒否してください。ZIPの外部属性からUNIXのファイル形式を確認し、シンボリックリンクのエントリを除外してください。- 必要な拡張子やディレクトリだけを許可リストで選んでください。
- 信頼できる親ディレクトリの下に、新しい専用の非公開ディレクトリを作ってください。ディレクトリの権限を制限し、ファイルは
wxで作成して上書きを防いでください。 - ライブラリにエントリのフィルターやシンボリックリンク対策があれば有効にしてください。
例
変更前
javascript
// 変更前: エントリのパスをそのまま使って展開します(Zip Slip)。
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
function extractUnsafe(zipPath, destDir) {
const zip = new AdmZip(zipPath);
zip.getEntries().forEach((entry) => {
const outPath = path.join(destDir, entry.entryName); // 検証なし
const data = entry.getData();
// entry.entryNameが"../../../../var/www/html/shell.js"なら展開先を脱出できます。
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, data); // 制限されていないパスにファイルを作成・上書きします。
});
}
変更後
javascript
// 変更後: 新しい専用ディレクトリでエントリ形式と最終パスを検証します。
const fs = require("fs");
const path = require("path");
const AdmZip = require("adm-zip");
function isUnixSymlink(entry) {
// ZIP外部属性の上位16ビットにはUNIXのファイルモードが入ります。
const unixMode = entry.header.attr >>> 16;
return (unixMode & 0o170000) === 0o120000;
}
function extractSafe(zipPath, destParent) {
const zip = new AdmZip(zipPath);
const parentReal = fs.realpathSync(destParent);
const destReal = fs.mkdtempSync(path.join(parentReal, "extract-"));
fs.chmodSync(destReal, 0o700);
for (const entry of zip.getEntries()) {
// 1) UNIXのシンボリックリンクを拒否します。
if (isUnixSymlink(entry)) continue;
// 2) パスを正規化します。
const normalized = path.normalize(entry.entryName);
// 3) 空・絶対パス、ドライブ接頭辞、親ディレクトリの要素を拒否します。
if (normalized === "" || normalized === ".") continue;
if (path.isAbsolute(normalized)) continue;
if (normalized.split(path.sep).includes("..")) continue;
if (/^[A-Za-z]:/.test(normalized)) continue;
// 4) 最終パスを解決し、展開先の内部か確認します。
const targetPath = path.resolve(destReal, normalized);
if (
path.relative(destReal, targetPath) !== ".." &&
!path.relative(destReal, targetPath).startsWith(".." + path.sep) &&
!path.isAbsolute(path.relative(destReal, targetPath))
) {
if (entry.isDirectory) {
fs.mkdirSync(targetPath, { recursive: true, mode: 0o700 });
continue;
}
// 5) 新しい専用ディレクトリ内だけに作成し、既存ファイルを上書きしません。
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
const data = entry.getData();
fs.writeFileSync(targetPath, data, { flag: "wx", mode: 0o600 });
}
}
return destReal;
}
説明:
- 変更前: 未検証の
entryNameをpath.joinで結合すると、../を含むエントリが展開先を脱出し、ファイルを作成・上書きする可能性があります。 - 変更後: 信頼する親の下に新しい専用ディレクトリを作り、UNIXのシンボリックリンクを拒否します。名前を正規化し、絶対パス、ドライブ接頭辞、親ディレクトリの要素を拒否したうえで、
path.resolveとpath.relativeで範囲を確認します。許可したファイルはwxで作成し、既存ファイルを上書きしません。