[PHP]passthru関数を完全解説!外部コマンド実行の使い方とセキュリティ対策

PHP

こんにちは!今回はPHPで外部コマンドを実行する関数の一つ、「passthru」について詳しく解説していきます。

passthru関数とは?

passthruは、外部プログラムやシェルコマンドを実行し、その生の出力を直接ブラウザに送信する関数です。画像やバイナリファイルなど、加工せずにそのまま出力したい場合に特に便利です。

基本的な使い方

構文

passthru(string $command, int &$result_code = null): false|null

パラメータ

  • $command: 実行したいコマンド文字列
  • $result_code: コマンドの終了ステータスコード(参照渡し)

戻り値

  • null: 成功時
  • false: 失敗時

基本的な使用例

<?php
// シンプルなコマンド実行
passthru("ls -la");

// 終了コードを取得
passthru("ls -la", $returnCode);

if ($returnCode === 0) {
    echo "コマンドが正常に実行されました";
} else {
    echo "エラーが発生しました: " . $returnCode;
}
?>

他のコマンド実行関数との違い

PHPには複数のコマンド実行関数があり、それぞれ用途が異なります。

関数出力の扱い用途
passthru()直接ブラウザに出力バイナリデータ、画像の出力
exec()最後の行のみ返す出力を変数に格納して処理
system()直接ブラウザに出力テキスト出力のコマンド
shell_exec()文字列として返す出力を文字列として取得
backtick (`)文字列として返すshell_exec()の省略記法

比較例

<?php
echo "=== passthru ===\n";
passthru("echo 'Hello World'");
// 出力: Hello World (直接表示)

echo "\n\n=== exec ===\n";
exec("echo 'Hello World'", $output);
print_r($output);
// 出力: Array ( [0] => Hello World )

echo "\n=== system ===\n";
system("echo 'Hello World'");
// 出力: Hello World (直接表示)

echo "\n\n=== shell_exec ===\n";
$result = shell_exec("echo 'Hello World'");
echo $result;
// 出力: Hello World (変数経由)
?>

実践的な使用例

例1: 画像の動的生成と出力

<?php
// ImageMagickを使って画像を生成
header('Content-Type: image/png');

$text = "Hello World";
$fontSize = 24;
$outputFile = tempnam(sys_get_temp_dir(), 'img') . '.png';

// ImageMagickコマンドで画像生成
$command = sprintf(
    "convert -size 300x100 xc:white -pointsize %d -gravity center -draw 'text 0,0 \"%s\"' %s",
    $fontSize,
    escapeshellarg($text),
    escapeshellarg($outputFile)
);

exec($command);

// 画像をブラウザに出力
passthru("cat " . escapeshellarg($outputFile));

// 一時ファイルを削除
unlink($outputFile);
?>

例2: PDFファイルの生成と出力

<?php
// HTMLからPDFを生成(wkhtmltopdfを使用)
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="report.pdf"');

$htmlFile = '/tmp/report.html';
$pdfFile = '/tmp/report.pdf';

// HTMLファイルを作成
file_put_contents($htmlFile, '<h1>レポート</h1><p>これはテストレポートです。</p>');

// PDFに変換
$command = sprintf(
    "wkhtmltopdf %s %s",
    escapeshellarg($htmlFile),
    escapeshellarg($pdfFile)
);

exec($command, $output, $returnCode);

if ($returnCode === 0 && file_exists($pdfFile)) {
    // PDFをブラウザに出力
    passthru("cat " . escapeshellarg($pdfFile));
    
    // 一時ファイルを削除
    unlink($htmlFile);
    unlink($pdfFile);
} else {
    header('Content-Type: text/plain');
    echo "PDFの生成に失敗しました";
}
?>

例3: ファイルの圧縮とダウンロード

<?php
// 複数ファイルをZIP圧縮してダウンロード
$files = ['/path/to/file1.txt', '/path/to/file2.txt', '/path/to/file3.txt'];
$zipFile = tempnam(sys_get_temp_dir(), 'archive') . '.zip';

// ZIPファイルを作成
$fileList = implode(' ', array_map('escapeshellarg', $files));
$command = "zip -j " . escapeshellarg($zipFile) . " " . $fileList;

exec($command, $output, $returnCode);

if ($returnCode === 0 && file_exists($zipFile)) {
    // ダウンロードヘッダーを設定
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="archive.zip"');
    header('Content-Length: ' . filesize($zipFile));
    
    // ZIPファイルを出力
    passthru("cat " . escapeshellarg($zipFile));
    
    // 一時ファイルを削除
    unlink($zipFile);
} else {
    echo "ZIPファイルの作成に失敗しました";
}
?>

例4: 動画のサムネイル生成

<?php
// FFmpegを使って動画のサムネイルを生成
$videoFile = '/path/to/video.mp4';
$thumbnailFile = tempnam(sys_get_temp_dir(), 'thumb') . '.jpg';

// 動画の5秒地点からサムネイルを抽出
$command = sprintf(
    "ffmpeg -i %s -ss 00:00:05 -vframes 1 %s 2>&1",
    escapeshellarg($videoFile),
    escapeshellarg($thumbnailFile)
);

exec($command, $output, $returnCode);

if ($returnCode === 0 && file_exists($thumbnailFile)) {
    header('Content-Type: image/jpeg');
    passthru("cat " . escapeshellarg($thumbnailFile));
    unlink($thumbnailFile);
} else {
    header('Content-Type: text/plain');
    echo "サムネイルの生成に失敗しました";
}
?>

例5: ログファイルの表示

<?php
// システムログの最新100行を表示
header('Content-Type: text/plain; charset=utf-8');

$logFile = '/var/log/application.log';

if (file_exists($logFile)) {
    echo "=== 最新ログ (最新100行) ===\n\n";
    passthru("tail -n 100 " . escapeshellarg($logFile));
} else {
    echo "ログファイルが見つかりません";
}
?>

例6: システム情報の表示

<?php
header('Content-Type: text/plain; charset=utf-8');

echo "=== システム情報 ===\n\n";

echo "【ディスク使用状況】\n";
passthru("df -h");

echo "\n\n【メモリ使用状況】\n";
passthru("free -h");

echo "\n\n【プロセス一覧(上位10件)】\n";
passthru("ps aux | head -n 11");

echo "\n\n【ネットワーク接続】\n";
passthru("netstat -tuln | head -n 20");
?>

重要なセキュリティ対策

1. コマンドインジェクション対策

最も重要: ユーザー入力を絶対にそのままコマンドに渡さない!

<?php
// ❌ 危険な例
$userInput = $_GET['file'];
passthru("cat " . $userInput); // コマンドインジェクションの危険性

// ✅ 安全な例
$userInput = $_GET['file'];
$safeInput = escapeshellarg($userInput);
passthru("cat " . $safeInput);

// さらに安全: ホワイトリストによる検証
$allowedFiles = ['file1.txt', 'file2.txt', 'file3.txt'];
if (in_array($userInput, $allowedFiles)) {
    passthru("cat " . escapeshellarg("/safe/path/" . $userInput));
} else {
    die("不正なファイル名です");
}
?>

2. escapeshellarg()の使用

<?php
// ユーザー入力をエスケープ
$filename = $_POST['filename'];
$safeFilename = escapeshellarg($filename);

passthru("cat " . $safeFilename);

// 複数の引数がある場合
$source = escapeshellarg($_POST['source']);
$destination = escapeshellarg($_POST['destination']);

passthru("cp " . $source . " " . $destination);
?>

3. escapeshellcmd()との違い

<?php
$userInput = "file.txt; rm -rf /";

// escapeshellarg - 引数全体をエスケープ(推奨)
$safe1 = escapeshellarg($userInput);
echo $safe1; // 'file.txt; rm -rf /'

// escapeshellcmd - コマンド全体をエスケープ
$safe2 = escapeshellcmd($userInput);
echo $safe2; // file.txt\; rm -rf /

// 推奨: escapeshellarg()を使用
passthru("cat " . escapeshellarg($userInput));
?>

4. ホワイトリスト方式の実装

<?php
class SafeCommandExecutor {
    private $allowedCommands = [
        'disk_usage' => 'df -h',
        'memory_info' => 'free -h',
        'date' => 'date',
        'uptime' => 'uptime'
    ];
    
    public function execute($commandName, &$returnCode = null) {
        if (!isset($this->allowedCommands[$commandName])) {
            throw new Exception("許可されていないコマンドです");
        }
        
        $command = $this->allowedCommands[$commandName];
        passthru($command, $returnCode);
        
        return $returnCode === 0;
    }
}

// 使用例
$executor = new SafeCommandExecutor();

try {
    header('Content-Type: text/plain');
    $executor->execute('disk_usage');
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}
?>

5. 権限の制限

<?php
// 特定のユーザーでコマンドを実行(sudoを使用)
function executeAsUser($command, $user) {
    $safeCommand = escapeshellarg($command);
    $safeUser = escapeshellarg($user);
    
    // sudoで特定ユーザーとして実行
    passthru("sudo -u " . $safeUser . " " . $safeCommand);
}

// 注意: sudoersファイルで適切な権限設定が必要
?>

エラーハンドリング

基本的なエラーハンドリング

<?php
$command = "some-command";

ob_start();
passthru($command . " 2>&1", $returnCode);
$output = ob_get_clean();

if ($returnCode !== 0) {
    error_log("コマンド実行エラー: " . $output);
    echo "処理に失敗しました";
} else {
    echo $output;
}
?>

タイムアウト処理

<?php
function executeWithTimeout($command, $timeout = 30) {
    // timeoutコマンドを使用
    $safeCommand = escapeshellarg($command);
    $timeoutCommand = "timeout " . (int)$timeout . " " . $command;
    
    passthru($timeoutCommand . " 2>&1", $returnCode);
    
    if ($returnCode === 124) {
        throw new Exception("コマンドがタイムアウトしました");
    } elseif ($returnCode !== 0) {
        throw new Exception("コマンドの実行に失敗しました: " . $returnCode);
    }
}

try {
    executeWithTimeout("sleep 5", 10); // 成功
    executeWithTimeout("sleep 15", 10); // タイムアウト
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage();
}
?>

ベストプラクティス

1. 出力バッファリングの制御

<?php
// 大きなファイルを出力する場合
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="largefile.zip"');

// バッファリングを無効化
if (ob_get_level()) {
    ob_end_clean();
}

// ファイルを出力
passthru("cat " . escapeshellarg($largeFile));
?>

2. ロギング

<?php
function loggedPassthru($command, &$returnCode = null) {
    $timestamp = date('Y-m-d H:i:s');
    $logFile = '/var/log/passthru.log';
    
    // コマンド実行前にログ
    file_put_contents(
        $logFile,
        "[{$timestamp}] 実行: {$command}\n",
        FILE_APPEND
    );
    
    // コマンド実行
    ob_start();
    passthru($command, $returnCode);
    $output = ob_get_clean();
    
    // 結果をログ
    file_put_contents(
        $logFile,
        "[{$timestamp}] 終了コード: {$returnCode}\n",
        FILE_APPEND
    );
    
    echo $output;
    return $returnCode;
}
?>

3. 環境変数の設定

<?php
// 安全な環境変数で実行
putenv('PATH=/usr/bin:/bin');
putenv('LANG=ja_JP.UTF-8');

passthru("your-command");
?>

注意点とトラブルシューティング

1. セーフモードと無効化されている場合

<?php
// passthruが利用可能かチェック
if (function_exists('passthru')) {
    // 無効化されていないかチェック
    $disabled = explode(',', ini_get('disable_functions'));
    
    if (in_array('passthru', $disabled)) {
        die("passthru関数は無効化されています");
    }
    
    passthru("ls -la");
} else {
    die("passthru関数は利用できません");
}
?>

2. Windows環境での考慮事項

<?php
// OSを判定
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
    // Windows
    passthru("dir /w");
} else {
    // Unix/Linux
    passthru("ls -la");
}
?>

3. 文字コードの問題

<?php
// 日本語を含むコマンド実行
header('Content-Type: text/plain; charset=utf-8');

// ロケールを設定
setlocale(LC_ALL, 'ja_JP.UTF-8');
putenv('LANG=ja_JP.UTF-8');

passthru("your-command");
?>

まとめ

passthru関数は外部コマンドの実行結果を直接出力する強力な関数ですが、使用には十分な注意が必要です。

重要ポイント:

  • バイナリデータの出力に最適: 画像、PDF、ZIPファイルなど
  • セキュリティが最優先: 必ずescapeshellarg()を使用
  • ユーザー入力の検証: ホワイトリスト方式を推奨
  • エラーハンドリング: 終了コードを必ずチェック
  • 代替手段の検討: 可能であればPHPネイティブの関数を使用

セキュリティチェックリスト:

  • ✅ ユーザー入力をescapeshellarg()でエスケープ
  • ✅ ホワイトリストによるコマンド/ファイルの検証
  • ✅ 適切なエラーハンドリング
  • ✅ ログの記録
  • ✅ 最小権限の原則を適用

外部コマンドの実行は便利ですが、セキュリティリスクも高いため、本当に必要な場合のみ使用し、必ず適切な対策を実施してください!


関連記事

  • exec() – コマンド実行結果を配列で取得
  • system() – コマンドを実行して出力を表示
  • shell_exec() – コマンド実行結果を文字列で取得
  • escapeshellarg() – シェル引数のエスケープ
タイトルとURLをコピーしました