[PHP]realpath関数の使い方を徹底解説!パス正規化とセキュリティ対策

PHP

はじめに

Webアプリケーション開発では、ファイルパスの扱いが非常に重要です。相対パス、シンボリックリンク、...を含むパスなど、さまざまな形式のパスを扱う必要があります。

PHPのrealpath関数は、これらの複雑なパスを正規化された絶対パスに変換してくれる便利な関数です。この記事では、realpath関数の基本から実践的な使い方、セキュリティ対策まで、分かりやすく解説していきます。

realpath関数とは?

realpath関数は、相対パスやシンボリックリンクを解決して、正規化された絶対パスを返す関数です。パスに含まれる.(カレントディレクトリ)、..(親ディレクトリ)、シンボリックリンクなどをすべて解決します。

基本的な構文

<?php
realpath(string $path): string|false
?>
  • $path: 正規化したいパス
  • 戻り値: 正規化された絶対パス、失敗時はfalse

最もシンプルな使用例

<?php
// 相対パスを絶対パスに変換
$absolutePath = realpath('./config/database.php');
echo $absolutePath;
// 出力例: /var/www/myapp/config/database.php
?>

realpath関数が解決するもの

realpath関数は以下のような要素を解決します:

1. 相対パスを絶対パスに変換

<?php
// カレントディレクトリが /var/www/myapp の場合

echo realpath('./config.php');
// /var/www/myapp/config.php

echo realpath('config/database.php');
// /var/www/myapp/config/database.php

echo realpath('../shared/utils.php');
// /var/www/shared/utils.php
?>

2. .と..を解決

<?php
$path = '/var/www/myapp/public/../config/./database.php';
echo realpath($path);
// /var/www/myapp/config/database.php

// 複雑な例
$path = '/var/www/./myapp/../myapp/public/../../myapp/config.php';
echo realpath($path);
// /var/www/myapp/config.php
?>

3. シンボリックリンクを解決

<?php
// /var/www/current が /var/www/releases/v1.2.3 へのシンボリックリンクの場合

echo realpath('/var/www/current/index.php');
// /var/www/releases/v1.2.3/index.php
?>

4. 重複したスラッシュを削除

<?php
$path = '/var//www///myapp////config.php';
echo realpath($path);
// /var/www/myapp/config.php
?>

重要な注意点:ファイルが存在しない場合

realpath関数の最も重要な特徴は、ファイルやディレクトリが存在しない場合はfalseを返すことです!

<?php
// 存在するファイル
$existing = realpath('/var/www/myapp/config.php');
echo $existing;  // /var/www/myapp/config.php

// 存在しないファイル
$nonExisting = realpath('/var/www/myapp/nonexistent.php');
var_dump($nonExisting);  // bool(false)
?>

この特性を理解していないと、思わぬバグの原因になるので注意が必要です!

実践的な使用例

1. セキュアなファイルパス検証

パストラバーサル攻撃を防ぐための最も効果的な方法:

<?php
function isPathSafe($basePath, $userPath) {
    // ベースパスの絶対パスを取得
    $realBase = realpath($basePath);
    
    if ($realBase === false) {
        return false;  // ベースパスが存在しない
    }
    
    // ユーザー指定パスの絶対パスを取得
    $fullPath = $basePath . '/' . $userPath;
    $realPath = realpath($fullPath);
    
    if ($realPath === false) {
        return false;  // ファイルが存在しない
    }
    
    // ベースパス配下にあるかチェック
    return strpos($realPath, $realBase) === 0;
}

// 使用例
$uploadDir = '/var/www/uploads';
$userFile = $_GET['file'] ?? '';

if (isPathSafe($uploadDir, $userFile)) {
    $filePath = realpath($uploadDir . '/' . $userFile);
    echo "安全なパス: {$filePath}\n";
    readfile($filePath);
} else {
    http_response_code(403);
    echo '不正なアクセスです';
}
?>

2. require/includeでの安全なファイル読み込み

<?php
class ConfigLoader {
    private $configDir;
    
    public function __construct($configDir) {
        $this->configDir = realpath($configDir);
        
        if ($this->configDir === false) {
            throw new Exception("設定ディレクトリが存在しません: {$configDir}");
        }
    }
    
    public function load($configName) {
        $configFile = $this->configDir . '/' . $configName . '.php';
        $realConfigFile = realpath($configFile);
        
        // セキュリティチェック
        if ($realConfigFile === false) {
            throw new Exception("設定ファイルが存在しません: {$configName}");
        }
        
        if (strpos($realConfigFile, $this->configDir) !== 0) {
            throw new Exception("不正なパスアクセス: {$configName}");
        }
        
        return require $realConfigFile;
    }
}

// 使用例
try {
    $loader = new ConfigLoader(__DIR__ . '/config');
    $dbConfig = $loader->load('database');
    print_r($dbConfig);
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}
?>

3. アップロードファイルの検証

<?php
function validateUploadPath($uploadedFile, $allowedDir) {
    // 許可ディレクトリの絶対パス
    $realAllowedDir = realpath($allowedDir);
    
    if ($realAllowedDir === false) {
        throw new Exception('アップロードディレクトリが存在しません');
    }
    
    // アップロードされたファイルの一時パス
    $tmpPath = $uploadedFile['tmp_name'];
    $fileName = basename($uploadedFile['name']);
    
    // 保存先パスを構築
    $destinationPath = $allowedDir . '/' . $fileName;
    
    // ファイル名のサニタイズ
    $safeFileName = preg_replace('/[^a-zA-Z0-9._-]/', '_', $fileName);
    $safePath = $allowedDir . '/' . $safeFileName;
    
    // ファイルを移動
    if (move_uploaded_file($tmpPath, $safePath)) {
        // 移動後のパスを検証
        $realPath = realpath($safePath);
        
        if ($realPath === false || strpos($realPath, $realAllowedDir) !== 0) {
            unlink($safePath);  // 安全でない場合は削除
            throw new Exception('不正なファイルパスです');
        }
        
        return $realPath;
    }
    
    throw new Exception('ファイルの移動に失敗しました');
}

// 使用例
if ($_FILES['upload']) {
    try {
        $savedPath = validateUploadPath($_FILES['upload'], '/var/www/uploads');
        echo "ファイルを保存しました: {$savedPath}";
    } catch (Exception $e) {
        echo "エラー: " . $e->getMessage();
    }
}
?>

4. プロジェクトルートの取得

<?php
/**
 * プロジェクトのルートディレクトリを取得
 */
function getProjectRoot() {
    // composer.jsonまたは.gitを探してプロジェクトルートを特定
    $current = __DIR__;
    
    while ($current !== '/') {
        if (file_exists($current . '/composer.json')) {
            return realpath($current);
        }
        
        $current = dirname($current);
    }
    
    return false;
}

// 使用例
$projectRoot = getProjectRoot();
if ($projectRoot) {
    echo "プロジェクトルート: {$projectRoot}\n";
    
    // ルートからの相対パスでファイルを読み込み
    $configPath = $projectRoot . '/config/app.php';
    if (file_exists($configPath)) {
        require realpath($configPath);
    }
}
?>

5. ログファイルのローテーション管理

<?php
class LogManager {
    private $logDir;
    
    public function __construct($logDir) {
        $this->logDir = realpath($logDir);
        
        if ($this->logDir === false) {
            throw new Exception("ログディレクトリが存在しません");
        }
    }
    
    public function getLogFiles($pattern = '*.log') {
        $files = glob($this->logDir . '/' . $pattern);
        $realFiles = [];
        
        foreach ($files as $file) {
            $realFile = realpath($file);
            
            // セキュリティチェック
            if ($realFile !== false && strpos($realFile, $this->logDir) === 0) {
                $realFiles[] = $realFile;
            }
        }
        
        return $realFiles;
    }
    
    public function getOldLogs($days = 30) {
        $files = $this->getLogFiles();
        $threshold = time() - ($days * 86400);
        $oldFiles = [];
        
        foreach ($files as $file) {
            if (filemtime($file) < $threshold) {
                $oldFiles[] = $file;
            }
        }
        
        return $oldFiles;
    }
}

// 使用例
$logManager = new LogManager('/var/log/myapp');
$oldLogs = $logManager->getOldLogs(30);

foreach ($oldLogs as $log) {
    echo "古いログ: {$log}\n";
}
?>

6. シンボリックリンクの実体を取得

<?php
function getActualPath($path) {
    $realPath = realpath($path);
    
    if ($realPath === false) {
        return [
            'exists' => false,
            'original' => $path,
            'message' => 'パスが存在しません'
        ];
    }
    
    return [
        'exists' => true,
        'original' => $path,
        'real' => $realPath,
        'is_symlink' => is_link($path),
        'is_file' => is_file($realPath),
        'is_dir' => is_dir($realPath)
    ];
}

// 使用例
$paths = [
    '/var/www/current',
    './config.php',
    '../shared/utils.php',
    '/nonexistent/path.php'
];

foreach ($paths as $path) {
    $info = getActualPath($path);
    print_r($info);
}
?>

よくあるエラーと対処法

エラー1: パスが存在しない

<?php
// ❌ 問題のあるコード
$path = realpath('/path/to/new/file.php');
// ファイルがまだ存在しないのでfalseが返る

// ✅ 解決策1: ディレクトリ部分のみを正規化
$dir = realpath(dirname('/path/to/new/file.php'));
if ($dir !== false) {
    $fullPath = $dir . '/' . basename('/path/to/new/file.php');
    file_put_contents($fullPath, 'content');
}

// ✅ 解決策2: 親ディレクトリを作成してから使用
$targetPath = '/var/www/new/directory/file.php';
$dir = dirname($targetPath);

if (!file_exists($dir)) {
    mkdir($dir, 0755, true);
}

$realDir = realpath($dir);
if ($realDir !== false) {
    $fullPath = $realDir . '/' . basename($targetPath);
    file_put_contents($fullPath, 'content');
}
?>

エラー2: 相対パスの基準が不明確

<?php
// ❌ カレントディレクトリに依存(予測不能)
$path = realpath('../config/app.php');

// ✅ __DIR__や__FILE__を基準にする
$path = realpath(__DIR__ . '/../config/app.php');

// ✅ 絶対パスで指定
$path = realpath('/var/www/myapp/config/app.php');
?>

エラー3: エラーハンドリングの欠如

<?php
// ❌ エラーチェックなし
$path = realpath($userInput);
require $path;  // $pathがfalseの場合エラー

// ✅ 適切なエラーハンドリング
$path = realpath($userInput);

if ($path === false) {
    throw new Exception("パスが無効です: {$userInput}");
}

if (!is_readable($path)) {
    throw new Exception("ファイルが読み取れません: {$path}");
}

require $path;
?>

パフォーマンスに関する考慮事項

realpath関数はファイルシステムにアクセスするため、頻繁に呼び出すとパフォーマンスに影響します:

<?php
// ❌ ループ内で何度も呼び出す
foreach ($files as $file) {
    $realPath = realpath($baseDir . '/' . $file);  // 毎回ファイルシステムアクセス
    processFile($realPath);
}

// ✅ ベースパスを事前に取得
$realBaseDir = realpath($baseDir);

if ($realBaseDir !== false) {
    foreach ($files as $file) {
        $fullPath = $realBaseDir . '/' . $file;
        
        // 必要な場合のみrealpath()を呼び出す
        if (is_link($fullPath)) {
            $fullPath = realpath($fullPath);
        }
        
        processFile($fullPath);
    }
}
?>

キャッシュの実装

<?php
class PathResolver {
    private static $cache = [];
    
    public static function resolve($path) {
        // キャッシュをチェック
        if (isset(self::$cache[$path])) {
            return self::$cache[$path];
        }
        
        // realpath()を実行
        $resolved = realpath($path);
        
        // 結果をキャッシュ(成功時のみ)
        if ($resolved !== false) {
            self::$cache[$path] = $resolved;
        }
        
        return $resolved;
    }
    
    public static function clearCache() {
        self::$cache = [];
    }
}

// 使用例
$path1 = PathResolver::resolve('./config/app.php');
$path2 = PathResolver::resolve('./config/app.php');  // キャッシュから取得
?>

代替関数との比較

関数シンボリックリンク存在しないパス...の解決用途
realpath()解決するfalseする完全な絶対パス取得
readlink()1段階のみ解決falseしないリンク先の取得
dirname()解決しない動作するしないディレクトリ部分の取得
basename()解決しない動作するしないファイル名の取得
<?php
$path = '/var/www/current/../config/./app.php';

echo realpath($path);     // /var/www/config/app.php (完全解決)
echo dirname($path);      // /var/www/current/../config/. (そのまま)
echo basename($path);     // app.php (ファイル名のみ)
?>

実用的な完全版パス検証クラス

<?php
/**
 * 安全なパス処理クラス
 */
class SecurePath {
    private $basePath;
    
    public function __construct($basePath) {
        $this->basePath = realpath($basePath);
        
        if ($this->basePath === false) {
            throw new Exception("ベースパスが存在しません: {$basePath}");
        }
    }
    
    /**
     * ユーザー入力パスを安全に解決
     */
    public function resolve($userPath) {
        // 空チェック
        if (empty($userPath)) {
            throw new Exception('パスが空です');
        }
        
        // NULLバイト攻撃の防止
        if (strpos($userPath, "\0") !== false) {
            throw new Exception('不正な文字が含まれています');
        }
        
        // 絶対パスの禁止(相対パスのみ許可)
        if ($userPath[0] === '/') {
            throw new Exception('絶対パスは使用できません');
        }
        
        // フルパスを構築
        $fullPath = $this->basePath . '/' . $userPath;
        
        // パスを正規化
        $realPath = realpath($fullPath);
        
        if ($realPath === false) {
            throw new Exception("パスが存在しません: {$userPath}");
        }
        
        // ベースパス配下にあるかチェック
        if (strpos($realPath, $this->basePath . '/') !== 0) {
            throw new Exception("ベースパス外へのアクセスは禁止されています");
        }
        
        return $realPath;
    }
    
    /**
     * ファイルの安全な読み込み
     */
    public function readFile($userPath) {
        $realPath = $this->resolve($userPath);
        
        if (!is_file($realPath)) {
            throw new Exception('ファイルではありません');
        }
        
        if (!is_readable($realPath)) {
            throw new Exception('ファイルが読み取れません');
        }
        
        return file_get_contents($realPath);
    }
    
    /**
     * ディレクトリの一覧取得
     */
    public function listDirectory($userPath = '') {
        $realPath = empty($userPath) ? $this->basePath : $this->resolve($userPath);
        
        if (!is_dir($realPath)) {
            throw new Exception('ディレクトリではありません');
        }
        
        $items = scandir($realPath);
        $result = [];
        
        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }
            
            $itemPath = $realPath . '/' . $item;
            $result[] = [
                'name' => $item,
                'path' => str_replace($this->basePath . '/', '', $itemPath),
                'type' => is_dir($itemPath) ? 'directory' : 'file',
                'size' => is_file($itemPath) ? filesize($itemPath) : null
            ];
        }
        
        return $result;
    }
    
    /**
     * ベースパスを取得
     */
    public function getBasePath() {
        return $this->basePath;
    }
}

// 使用例
try {
    $storage = new SecurePath('/var/www/uploads');
    
    // ファイルの読み込み
    $content = $storage->readFile('documents/report.pdf');
    
    // ディレクトリ一覧
    $files = $storage->listDirectory('documents');
    foreach ($files as $file) {
        echo "{$file['name']} ({$file['type']})\n";
    }
    
    // 危険なパスは拒否される
    // $storage->resolve('../../../etc/passwd');  // 例外がスローされる
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}
?>

まとめ

realpath関数のポイントをおさらいしましょう:

  1. 相対パス、シンボリックリンク、...を解決して絶対パスを返す
  2. ファイル/ディレクトリが存在しない場合はfalseを返す(重要!)
  3. パストラバーサル攻撃の防止に非常に有効
  4. セキュリティチェックの基本ツール
  5. エラーハンドリングは必須
  6. 頻繁に呼び出す場合はキャッシュを検討
  7. 存在しないパスには使えないので注意

realpath関数は、安全なファイル操作を実現するための最も重要な関数の一つです。適切に使用して、セキュアなWebアプリケーションを構築しましょう!

参考リンク


この記事が役に立ったら、ぜひシェアしてください!PHPに関する他の記事もお楽しみに。

タイトルとURLをコピーしました