[PHP]posix_setuid関数とは?ユーザーID変更を徹底解説

PHP

こんにちは!今回はPHPのPOSIX拡張モジュールに含まれる「posix_setuid」関数について、詳しく解説していきます。この関数は、プロセスの実行ユーザーを変更する重要な機能で、セキュリティとアクセス制御において極めて重要な役割を果たします。

posix_setuidとは何か?

posix_setuidは、現在のプロセスの実効ユーザーID(EUID)を設定する関数です。これにより、プロセスが特定のユーザーの権限で動作するように変更できます。

基本的な構文

posix_setuid(int $user_id): bool

パラメータ:

  • $user_id: 設定したいユーザーID(UID)

戻り値:

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

ユーザーIDの種類

Unixシステムでは、プロセスは3種類のユーザーIDを持っています:

1. 実ユーザーID (Real UID – RUID)

プロセスを起動した実際のユーザーのID

2. 実効ユーザーID (Effective UID – EUID)

プロセスの権限チェックに使用されるID

3. 保存セットユーザーID (Saved UID – SUID)

権限を一時的に下げた後、元に戻すために保存されるID

<?php
// 現在のユーザーIDを確認
echo "実ユーザーID (RUID): " . posix_getuid() . "\n";
echo "実効ユーザーID (EUID): " . posix_geteuid() . "\n";

// ユーザー名も表示
$userInfo = posix_getpwuid(posix_geteuid());
echo "実効ユーザー名: " . $userInfo['name'] . "\n";
?>

基本的な使用例

例1: ユーザーの切り替え

<?php
// 現在の情報を表示
function showCurrentUser() {
    $euid = posix_geteuid();
    $userInfo = posix_getpwuid($euid);
    echo "現在の実効ユーザー: {$userInfo['name']} (UID: {$euid})\n";
}

echo "=== 初期状態 ===\n";
showCurrentUser();

// rootとして実行されている場合のみ有効
if (posix_geteuid() === 0) {
    echo "\n=== www-dataユーザーに切り替え ===\n";
    
    // www-dataのUIDを取得
    $wwwUserInfo = posix_getpwnam('www-data');
    
    if ($wwwUserInfo) {
        if (posix_setuid($wwwUserInfo['uid'])) {
            showCurrentUser();
            echo "ユーザーの切り替えに成功しました\n";
        } else {
            echo "ユーザーの切り替えに失敗しました\n";
        }
    }
} else {
    echo "\nroot権限が必要です\n";
}
?>

例2: 権限の降格(特権の放棄)

<?php
// rootから一般ユーザーへの権限降格
function dropPrivileges($username) {
    // rootでない場合は何もしない
    if (posix_geteuid() !== 0) {
        echo "root権限がありません\n";
        return false;
    }
    
    // ユーザー情報を取得
    $userInfo = posix_getpwnam($username);
    if (!$userInfo) {
        echo "ユーザー '{$username}' が見つかりません\n";
        return false;
    }
    
    echo "権限を降格します: root -> {$username}\n";
    
    // グループIDを変更(先にgidを変更する必要がある)
    if (!posix_setgid($userInfo['gid'])) {
        echo "グループIDの変更に失敗しました\n";
        return false;
    }
    
    // 補助グループを設定
    if (function_exists('posix_setgroups')) {
        $groups = posix_getgroups();
        posix_setgroups([$userInfo['gid']]);
    }
    
    // ユーザーIDを変更
    if (!posix_setuid($userInfo['uid'])) {
        echo "ユーザーIDの変更に失敗しました\n";
        return false;
    }
    
    // 確認
    $currentUser = posix_getpwuid(posix_geteuid());
    echo "権限降格成功: {$currentUser['name']} (UID: {$userInfo['uid']})\n";
    
    return true;
}

// 使用例
dropPrivileges('www-data');

// この時点でrootに戻ることはできない
if (posix_setuid(0) === false) {
    echo "root権限に戻れません(正常な動作)\n";
}
?>

実践的な使用例

例1: セキュアなWebサーバー起動スクリプト

<?php
class SecureServer {
    private $config;
    
    public function __construct($config) {
        $this->config = $config;
    }
    
    public function start() {
        // rootとして起動されているか確認
        if (posix_geteuid() !== 0) {
            die("このスクリプトはroot権限で実行する必要があります\n");
        }
        
        echo "=== セキュアサーバー起動 ===\n";
        
        // 特権ポートをバインド(例: 80番ポート)
        $this->bindPrivilegedPort();
        
        // 権限を降格
        $this->dropPrivileges();
        
        // メインサーバーループ
        $this->runServer();
    }
    
    private function bindPrivilegedPort() {
        $port = $this->config['port'];
        echo "ポート {$port} をバインド中...\n";
        
        // 実際のソケット処理
        // socket_create(), socket_bind() など
        
        echo "ポート {$port} のバインドに成功しました\n";
    }
    
    private function dropPrivileges() {
        $username = $this->config['run_as_user'];
        $userInfo = posix_getpwnam($username);
        
        if (!$userInfo) {
            die("ユーザー '{$username}' が見つかりません\n");
        }
        
        echo "権限を降格中: root -> {$username}\n";
        
        // グループを先に変更
        if (!posix_setgid($userInfo['gid'])) {
            die("グループID変更に失敗しました\n");
        }
        
        // 補助グループをクリア
        if (function_exists('posix_setgroups')) {
            posix_setgroups([$userInfo['gid']]);
        }
        
        // ユーザーIDを変更
        if (!posix_setuid($userInfo['uid'])) {
            die("ユーザーID変更に失敗しました\n");
        }
        
        // 確認
        $this->verifyPrivilegeDrop();
        
        echo "権限降格成功\n";
    }
    
    private function verifyPrivilegeDrop() {
        // rootに戻れないことを確認
        if (posix_setuid(0) === true) {
            die("エラー: root権限に戻ることができました(セキュリティリスク)\n");
        }
        
        // 現在のユーザーを確認
        $euid = posix_geteuid();
        if ($euid === 0) {
            die("エラー: まだroot権限を持っています\n");
        }
        
        $userInfo = posix_getpwuid($euid);
        echo "検証成功: {$userInfo['name']} として実行中\n";
    }
    
    private function runServer() {
        echo "サーバーを起動しました\n";
        echo "実行ユーザー: " . posix_getpwuid(posix_geteuid())['name'] . "\n";
        
        // サーバーのメインループ
        while (true) {
            // リクエストを処理
            // この時点では一般ユーザーの権限で動作
            sleep(1);
        }
    }
}

// 設定
$config = [
    'port' => 80,
    'run_as_user' => 'www-data'
];

// サーバー起動
$server = new SecureServer($config);
$server->start();
?>

例2: ファイル処理デーモン

<?php
class FileProcessorDaemon {
    private $processUser;
    private $inputDir;
    private $outputDir;
    
    public function __construct($config) {
        $this->processUser = $config['user'];
        $this->inputDir = $config['input_dir'];
        $this->outputDir = $config['output_dir'];
    }
    
    public function start() {
        // rootとして起動されている必要がある
        if (posix_geteuid() !== 0) {
            die("root権限で実行してください\n");
        }
        
        // 必要なディレクトリを作成(root権限で)
        $this->setupDirectories();
        
        // 権限を降格
        $this->switchToProcessUser();
        
        // デーモン化
        $this->daemonize();
        
        // メイン処理ループ
        $this->processLoop();
    }
    
    private function setupDirectories() {
        echo "ディレクトリをセットアップ中...\n";
        
        // ディレクトリを作成
        if (!is_dir($this->inputDir)) {
            mkdir($this->inputDir, 0755, true);
        }
        if (!is_dir($this->outputDir)) {
            mkdir($this->outputDir, 0755, true);
        }
        
        // 所有者を変更
        $userInfo = posix_getpwnam($this->processUser);
        chown($this->inputDir, $userInfo['uid']);
        chown($this->outputDir, $userInfo['uid']);
        chgrp($this->inputDir, $userInfo['gid']);
        chgrp($this->outputDir, $userInfo['gid']);
        
        echo "ディレクトリセットアップ完了\n";
    }
    
    private function switchToProcessUser() {
        echo "ユーザーを切り替え中: root -> {$this->processUser}\n";
        
        $userInfo = posix_getpwnam($this->processUser);
        if (!$userInfo) {
            die("ユーザーが見つかりません: {$this->processUser}\n");
        }
        
        // グループIDを変更
        if (!posix_setgid($userInfo['gid'])) {
            $error = posix_strerror(posix_get_last_error());
            die("グループID変更失敗: {$error}\n");
        }
        
        // ユーザーIDを変更
        if (!posix_setuid($userInfo['uid'])) {
            $error = posix_strerror(posix_get_last_error());
            die("ユーザーID変更失敗: {$error}\n");
        }
        
        echo "ユーザー切り替え完了: {$this->processUser}\n";
    }
    
    private function daemonize() {
        $pid = pcntl_fork();
        if ($pid == -1) {
            die("フォーク失敗\n");
        }
        if ($pid) {
            exit(0); // 親プロセス終了
        }
        
        posix_setsid();
        
        $pid = pcntl_fork();
        if ($pid == -1) {
            die("2回目のフォーク失敗\n");
        }
        if ($pid) {
            exit(0);
        }
        
        chdir('/');
        umask(0);
        
        echo "デーモン化完了\n";
    }
    
    private function processLoop() {
        $logFile = '/var/log/file_processor.log';
        
        while (true) {
            // ファイルをスキャン
            $files = glob($this->inputDir . '/*');
            
            foreach ($files as $file) {
                $this->processFile($file);
            }
            
            sleep(5);
        }
    }
    
    private function processFile($file) {
        $basename = basename($file);
        $outputFile = $this->outputDir . '/' . $basename;
        
        // ファイルを処理(一般ユーザー権限で)
        $content = file_get_contents($file);
        $processed = strtoupper($content); // 例: 大文字変換
        
        file_put_contents($outputFile, $processed);
        unlink($file);
        
        $this->log("処理完了: {$basename}");
    }
    
    private function log($message) {
        $timestamp = date('Y-m-d H:i:s');
        $user = posix_getpwuid(posix_geteuid())['name'];
        $log = "[{$timestamp}] [{$user}] {$message}\n";
        file_put_contents('/var/log/file_processor.log', $log, FILE_APPEND);
    }
}

// 使用例
$config = [
    'user' => 'processor',
    'input_dir' => '/var/spool/processor/input',
    'output_dir' => '/var/spool/processor/output'
];

$daemon = new FileProcessorDaemon($config);
$daemon->start();
?>

例3: タスクランナー(異なるユーザーでタスクを実行)

<?php
class TaskRunner {
    private $tasks = [];
    
    public function addTask($name, $user, $callback) {
        $this->tasks[] = [
            'name' => $name,
            'user' => $user,
            'callback' => $callback
        ];
    }
    
    public function run() {
        // rootで起動されている必要がある
        if (posix_geteuid() !== 0) {
            die("root権限が必要です\n");
        }
        
        foreach ($this->tasks as $task) {
            $this->runTask($task);
        }
    }
    
    private function runTask($task) {
        echo "\n=== タスク実行: {$task['name']} ===\n";
        
        // 子プロセスを作成
        $pid = pcntl_fork();
        
        if ($pid == -1) {
            echo "フォーク失敗\n";
            return;
        }
        
        if ($pid == 0) {
            // 子プロセス
            $this->runAsUser($task['user'], $task['callback']);
            exit(0);
        } else {
            // 親プロセス:子プロセスの終了を待つ
            pcntl_waitpid($pid, $status);
            echo "タスク '{$task['name']}' 完了\n";
        }
    }
    
    private function runAsUser($username, $callback) {
        $userInfo = posix_getpwnam($username);
        
        if (!$userInfo) {
            echo "ユーザー '{$username}' が見つかりません\n";
            return;
        }
        
        echo "ユーザー '{$username}' として実行中...\n";
        
        // グループIDを変更
        posix_setgid($userInfo['gid']);
        
        // ユーザーIDを変更
        if (!posix_setuid($userInfo['uid'])) {
            echo "ユーザーID変更失敗\n";
            return;
        }
        
        // ホームディレクトリに移動
        chdir($userInfo['dir']);
        
        // 環境変数を設定
        putenv("HOME={$userInfo['dir']}");
        putenv("USER={$username}");
        putenv("LOGNAME={$username}");
        
        // 実行ユーザーを確認
        $currentUser = posix_getpwuid(posix_geteuid());
        echo "実行ユーザー: {$currentUser['name']}\n";
        
        // コールバックを実行
        call_user_func($callback);
    }
}

// 使用例
$runner = new TaskRunner();

// タスク1: www-dataとしてログをクリーンアップ
$runner->addTask('ログクリーンアップ', 'www-data', function() {
    echo "古いログファイルを削除中...\n";
    // 実際のクリーンアップ処理
    $logDir = '/var/log/webapp';
    if (is_dir($logDir)) {
        $files = glob($logDir . '/*.old');
        foreach ($files as $file) {
            unlink($file);
            echo "削除: {$file}\n";
        }
    }
});

// タスク2: backupユーザーとしてバックアップ
$runner->addTask('バックアップ', 'backup', function() {
    echo "バックアップを作成中...\n";
    // 実際のバックアップ処理
    $source = '/var/data';
    $dest = '/backup/data_' . date('Y-m-d') . '.tar.gz';
    echo "バックアップ先: {$dest}\n";
});

// タスク3: reportユーザーとしてレポート生成
$runner->addTask('レポート生成', 'report', function() {
    echo "レポートを生成中...\n";
    // 実際のレポート生成処理
    $report = "レポート内容\n";
    file_put_contents('/tmp/report.txt', $report);
    echo "レポート生成完了\n";
});

// 全タスクを実行
$runner->run();
?>

例4: セキュアなCGIハンドラー

<?php
class SecureCGIHandler {
    private $scriptDir;
    private $allowedUsers;
    
    public function __construct($config) {
        $this->scriptDir = $config['script_dir'];
        $this->allowedUsers = $config['allowed_users'];
    }
    
    public function executeScript($scriptPath, $scriptUser) {
        // rootとして実行されているか確認
        if (posix_geteuid() !== 0) {
            throw new Exception("root権限が必要です");
        }
        
        // ユーザーが許可リストにあるか確認
        if (!in_array($scriptUser, $this->allowedUsers)) {
            throw new Exception("許可されていないユーザーです: {$scriptUser}");
        }
        
        // スクリプトパスの検証
        $fullPath = realpath($this->scriptDir . '/' . $scriptPath);
        if (strpos($fullPath, realpath($this->scriptDir)) !== 0) {
            throw new Exception("不正なパスです");
        }
        
        if (!file_exists($fullPath)) {
            throw new Exception("スクリプトが見つかりません");
        }
        
        // 子プロセスで実行
        $pid = pcntl_fork();
        
        if ($pid == -1) {
            throw new Exception("フォーク失敗");
        }
        
        if ($pid == 0) {
            // 子プロセス
            $this->runScriptAsUser($fullPath, $scriptUser);
            exit(0);
        } else {
            // 親プロセス
            pcntl_waitpid($pid, $status);
            return pcntl_wexitstatus($status);
        }
    }
    
    private function runScriptAsUser($scriptPath, $username) {
        // ユーザー情報を取得
        $userInfo = posix_getpwnam($username);
        if (!$userInfo) {
            die("ユーザーが見つかりません: {$username}\n");
        }
        
        // スクリプトの所有者を確認
        $scriptOwner = fileowner($scriptPath);
        if ($scriptOwner !== $userInfo['uid']) {
            die("スクリプトの所有者が一致しません\n");
        }
        
        // リソース制限を設定
        posix_setrlimit(POSIX_RLIMIT_CPU, 30, 30); // CPU 30秒
        posix_setrlimit(POSIX_RLIMIT_AS, 128*1024*1024, 128*1024*1024); // メモリ 128MB
        
        // グループIDを変更
        if (!posix_setgid($userInfo['gid'])) {
            die("グループID変更失敗\n");
        }
        
        // ユーザーIDを変更
        if (!posix_setuid($userInfo['uid'])) {
            die("ユーザーID変更失敗\n");
        }
        
        // 環境変数をクリーンアップ
        $safeEnv = [
            'PATH' => '/usr/local/bin:/usr/bin:/bin',
            'HOME' => $userInfo['dir'],
            'USER' => $username,
            'LOGNAME' => $username
        ];
        
        foreach ($_ENV as $key => $value) {
            putenv($key);
        }
        
        foreach ($safeEnv as $key => $value) {
            putenv("{$key}={$value}");
        }
        
        // スクリプトを実行
        chdir(dirname($scriptPath));
        include $scriptPath;
    }
}

// 使用例
$handler = new SecureCGIHandler([
    'script_dir' => '/var/www/cgi-bin',
    'allowed_users' => ['www-data', 'webapp', 'scriptuser']
]);

try {
    $exitCode = $handler->executeScript('user_script.php', 'webapp');
    echo "スクリプト実行完了: 終了コード {$exitCode}\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

セキュリティのベストプラクティス

1. 権限降格の順序

<?php
// 正しい順序で権限を降格
function secureDropPrivileges($username) {
    $userInfo = posix_getpwnam($username);
    
    // 1. 補助グループをクリア(オプション)
    if (function_exists('posix_setgroups')) {
        posix_setgroups([$userInfo['gid']]);
    }
    
    // 2. グループIDを変更(先に!)
    if (!posix_setgid($userInfo['gid'])) {
        die("グループID変更失敗\n");
    }
    
    // 3. ユーザーIDを変更(後で!)
    if (!posix_setuid($userInfo['uid'])) {
        die("ユーザーID変更失敗\n");
    }
    
    // 4. 検証
    if (posix_geteuid() === 0) {
        die("エラー: まだroot権限があります\n");
    }
    
    // 5. root に戻れないことを確認
    if (posix_setuid(0) !== false) {
        die("エラー: root権限に戻れてしまいました\n");
    }
    
    return true;
}
?>

2. 権限降格の検証

<?php
function verifyPrivilegeDrop($expectedUser) {
    // 実効UIDを確認
    $euid = posix_geteuid();
    $userInfo = posix_getpwnam($expectedUser);
    
    if ($euid !== $userInfo['uid']) {
        throw new Exception("権限降格の検証失敗: 期待={$expectedUser}, 実際=" . posix_getpwuid($euid)['name']);
    }
    
    // rootに戻れないことを確認
    if (@posix_setuid(0) === true) {
        throw new Exception("セキュリティエラー: root権限に戻れます");
    }
    
    // ファイル作成のテスト
    $testFile = '/root/test.txt';
    if (@file_put_contents($testFile, 'test')) {
        @unlink($testFile);
        throw new Exception("セキュリティエラー: root領域に書き込めます");
    }
    
    echo "権限降格の検証成功: {$expectedUser}\n";
    return true;
}
?>

3. エラーハンドリング

<?php
function safeSetuid($username) {
    $userInfo = posix_getpwnam($username);
    
    if (!$userInfo) {
        error_log("ユーザーが存在しません: {$username}");
        return false;
    }
    
    // グループIDを変更
    if (!posix_setgid($userInfo['gid'])) {
        $errno = posix_get_last_error();
        $error = posix_strerror($errno);
        error_log("グループID変更失敗 ({$errno}): {$error}");
        return false;
    }
    
    // ユーザーIDを変更
    if (!posix_setuid($userInfo['uid'])) {
        $errno = posix_get_last_error();
        $error = posix_strerror($errno);
        error_log("ユーザーID変更失敗 ({$errno}): {$error}");
        return false;
    }
    
    return true;
}
?>

よくあるエラーと対処法

エラー1: EPERM (Operation not permitted)

<?php
// 一般ユーザーが他のユーザーに変更しようとした
$currentUser = posix_getpwuid(posix_geteuid())['name'];

if (posix_geteuid() !== 0) {
    echo "エラー: {$currentUser}ユーザーは他のユーザーに変更できません\n";
    echo "解決策: rootとして実行するか、sudo を使用してください\n";
}

// root権限があれば変更可能
if (posix_geteuid() === 0) {
    posix_setuid(1000); // 成功
}
?>

エラー2: 無効なUID

<?php
function setuidWithValidation($uid) {
    // UIDの存在を確認
    $userInfo = posix_getpwuid($uid);
    
    if (!$userInfo) {
        echo "エラー: UID {$uid} は存在しません\n";
        echo "有効なユーザー一覧:\n";
        
        // /etc/passwdから読み取り
        $passwd = file('/etc/passwd');
        foreach ($passwd as $line) {
            $parts = explode(':', $line);
            echo "  {$parts[0]} (UID: {$parts[2]})\n";
        }
        
        return false;
    }
    
    return posix_setuid($uid);
}
?>

エラー3: 権限降格後にroot権限が必要な操作

<?php
class PrivilegedOperation {
    private $originalEuid;
    
    public function __construct() {
        $this->originalEuid = posix_geteuid();
    }
    
    public function needRootFor($callback) {
        if ($this->originalEuid !== 0) {
            throw new Exception("rootとして起動されていません");
        }
        
        echo "警告: 権限降格後はroot操作はできません\n";
        echo "解決策: root操作を先に実行してください\n";
        
        // root操作を先に実行
        $callback();
        
        // その後で権限を降格
        // posix_setuid() を呼ぶと二度とrootに戻れない
    }
}
?>

まとめ

posix_setuidは、PHPでユーザー権限を制御するための重要な関数です。

重要なポイント:

  • root権限からのみ他のユーザーに変更可能
  • 一度権限を降格すると元に戻せない
  • グループIDを先に変更する必要がある
  • セキュリティ検証を必ず実施する

主な用途:

  • Webサーバーの権限分離
  • デーモンプロセスのセキュリティ強化
  • CGIスクリプトの安全な実行
  • タスクランナーでの権限制御

セキュリティの鉄則:

  1. 必要最小限の権限で動作させる
  2. 権限降格後は検証を行う
  3. root操作は先に済ませる
  4. エラーハンドリングを必ず実装する

適切な権限管理により、システムのセキュリティを大幅に向上させることができます。特に本番環境では、posix_setuidを使った権限分離が推奨されます!


関連記事:

  • posix_setgid(): グループIDを設定
  • posix_geteuid(): 実効ユーザーIDを取得
  • posix_getuid(): 実ユーザーIDを取得
  • posix_getpwnam(): ユーザー名からユーザー情報を取得
  • posix_getpwuid(): UIDからユーザー情報を取得
タイトルとURLをコピーしました