ディレクトリの制限不備によるパストラバーサル

許可したディレクトリ内にファイルアクセスを制限する

説明

入力値をファイルパスとして使う際に、許可したディレクトリの境界を確認しないと、パストラバーサルが発生します。攻撃者は../や..\\などの相対パス要素を使い、範囲外のファイルにアクセスする可能性があります。

想定される影響

影響はアプリケーションのファイル権限と読み書きの処理によって変わります。

  • システムファイルや他の利用者のデータの漏えい
  • データや設定の不正な読み取り・変更
  • 攻撃者が制御するファイルをアプリケーションが実行する場合のコード実行

対処方法

  1. 入力値を直接パスにせず、検証して許可したディレクトリ内にアクセスを限定してください。
  2. Path.resolve()で結合した後に正規化し、基準パスとPathの構成要素単位で比較してください。resolve()だけでは範囲内か確認できません。既存ファイルではtoRealPath()でシンボリックリンクを解決し、実際のアクセス先も確認します。
  3. 検証後に攻撃者がディレクトリを変更できる場合は、権限を制限し、SecureDirectoryStreamなどディレクトリを基準とするAPIを検討してください。
  4. 利用者がパスを選ぶ必要がなければ、UUIDなどを基にサーバー側でファイル名を生成してください。

例

変更前

java
private static final String BASE_PATH ="/restricted/directory";

public String readFile(String filePath) throws IOException {
    Path file = Paths.get("/restricted/directory/" + filePath);
    return new String(Files.readAllBytes(file));
}

File(parent, child)の第2引数もパスの一部です。親が固定でも、信頼できないchildに..が含まれると、正規化後に基準ディレクトリの外へ出る可能性があります。

java
import java.io.File;
import javax.servlet.http.HttpServletRequest;

public final class UnsafeUploadPath {
    private static final File UPLOAD_DIR = new File("/var/app/uploads");

    public File destination(HttpServletRequest request) {
        String child = request.getParameter("name");

        // 危険: 第2引数も攻撃者が制御するパスです。
        return new File(UPLOAD_DIR, child);
    }
}

変更後

Java 11以降の例です。攻撃者が基準ディレクトリやその配下のパスを変更できないことを前提とします。ファイルサイズの制限と不正入力への応答は別途設けてください。

java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;

public final class SafeFileReader {
    private static final Path BASE_PATH = Path.of("/restricted/directory");

    public String readFile(String filePath) throws IOException {
        Path base = BASE_PATH.toRealPath();
        Path supplied = Path.of(filePath);
        if (supplied.isAbsolute()) {
            throw new SecurityException("Absolute paths are not allowed.");
        }

        Path candidate = base.resolve(supplied).normalize();
        if (!candidate.startsWith(base)) {
            throw new SecurityException("Unauthorized access attempt detected.");
        }

        // 既存のリンクを解決し、実際のアクセス先も基準ディレクトリ内に限定します。
        Path requestedPath = candidate.toRealPath();
        if (!requestedPath.startsWith(base)
                || !Files.isRegularFile(requestedPath, LinkOption.NOFOLLOW_LINKS)) {
            throw new SecurityException("Unauthorized access attempt detected.");
        }
        return Files.readString(requestedPath, StandardCharsets.UTF_8);
    }
}

変更前はnew File(parent, child)を含め、入力値を直接パスとして使います。変更後は絶対パスを拒否し、Path.startsWith(Path)で正規化した構成要素を比較します。さらにリンクを解決して実際のアクセス先を確認し、検証したrequestedPath自体を読み取ります。

関連CVE

参考資料