[PHP]posix_setegid関数とは?実効グループIDを設定する方法を徹底解説

PHP

こんにちは!今回はPHPのPOSIX関数の中から、posix_setegid関数について詳しく解説していきます。プロセスの実効グループID(Effective GID)を設定し、アクセス権限を細かく制御する方法を、実践的なサンプルコードと共にお伝えします。

posix_setegid関数とは?

posix_setegidは、現在のプロセスの**実効グループID(Effective GID)**を設定する関数です。ファイルアクセス権限のチェックに使用されるグループIDを一時的に変更できます。

基本構文

posix_setegid(int $group_id): bool
  • 引数: 設定したいグループID(整数)
  • 戻り値: 成功時にtrue、失敗時にfalse

実グループIDと実効グループIDの違い

Unix/Linuxシステムには2種類のグループIDがあります。

グループIDの種類

<?php
// 実グループID (Real GID)
// - プロセスを起動した実際のユーザーのグループ
// - posix_getgid()で取得
$real_gid = posix_getgid();

// 実効グループID (Effective GID)
// - 実際のアクセス権限チェックに使用されるグループID
// - posix_getegid()で取得
$effective_gid = posix_getegid();

echo "実グループID: {$real_gid}\n";
echo "実効グループID: {$effective_gid}\n";
?>

なぜ2つのIDが必要?

実効グループIDの仕組みにより、一時的に異なるグループの権限でファイルにアクセスできます。

<?php
// 例: ユーザーがグループ'developers' (GID: 1000)で実行
// しかし一時的に 'database' (GID: 2000)グループの権限が必要

$original_egid = posix_getegid(); // 1000

// データベースグループの権限に変更
posix_setegid(2000);

// データベースファイルにアクセス(グループ権限で)
$data = file_get_contents('/var/db/data.db');

// 元の権限に戻す
posix_setegid($original_egid);
?>

実践的な使い方

例1: 基本的な実効GIDの変更

<?php
// 現在の権限情報を表示
function displayGroupInfo() {
    $rgid = posix_getgid();
    $egid = posix_getegid();
    
    $rgroup = posix_getgrgid($rgid);
    $egroup = posix_getgrgid($egid);
    
    echo "実グループID: {$rgid} ({$rgroup['name']})\n";
    echo "実効グループID: {$egid} ({$egroup['name']})\n";
    echo "\n";
}

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

// 実効GIDを変更(例: www-dataグループに変更)
$www_data = posix_getgrnam('www-data');
if ($www_data) {
    echo "=== www-dataグループに変更 ===\n";
    if (posix_setegid($www_data['gid'])) {
        displayGroupInfo();
        
        // 元に戻す
        echo "=== 元のグループに戻す ===\n";
        posix_setegid(posix_getgid());
        displayGroupInfo();
    } else {
        echo "実効GIDの変更に失敗しました\n";
        $error = posix_strerror(posix_get_last_error());
        echo "エラー: {$error}\n";
    }
}
?>

例2: 一時的な権限昇格クラス

<?php
class TemporaryGroupPrivilege {
    private $original_egid;
    private $changed = false;
    
    public function __construct($group_name_or_gid) {
        $this->original_egid = posix_getegid();
        
        // グループIDを取得
        if (is_string($group_name_or_gid)) {
            $group_info = posix_getgrnam($group_name_or_gid);
            if (!$group_info) {
                throw new Exception("グループが見つかりません: {$group_name_or_gid}");
            }
            $gid = $group_info['gid'];
        } else {
            $gid = $group_name_or_gid;
        }
        
        // 実効GIDを変更
        if (!posix_setegid($gid)) {
            $error = posix_strerror(posix_get_last_error());
            throw new Exception("実効GIDの変更に失敗: {$error}");
        }
        
        $this->changed = true;
    }
    
    public function __destruct() {
        // 元の実効GIDに戻す
        if ($this->changed) {
            posix_setegid($this->original_egid);
        }
    }
}

// 使用例
try {
    echo "通常の権限で実行中...\n";
    
    // 一時的にwww-dataグループの権限に変更
    {
        $temp = new TemporaryGroupPrivilege('www-data');
        echo "www-dataグループの権限で実行中...\n";
        
        // この範囲内でwww-dataグループの権限でファイルにアクセス
        // ...
        
    } // スコープを抜けると自動的に元の権限に戻る
    
    echo "通常の権限に戻りました\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例3: セキュアなファイルアクセス

<?php
class SecureFileAccess {
    public function readGroupFile($filepath, $group_name) {
        // ファイルの存在確認
        if (!file_exists($filepath)) {
            throw new Exception("ファイルが見つかりません: {$filepath}");
        }
        
        // ファイルのグループ所有権を確認
        $file_gid = filegroup($filepath);
        $group_info = posix_getgrgid($file_gid);
        
        echo "ファイル: {$filepath}\n";
        echo "所有グループ: {$group_info['name']} (GID: {$file_gid})\n";
        
        // 元の実効GIDを保存
        $original_egid = posix_getegid();
        
        // 指定されたグループの権限に変更
        $target_group = posix_getgrnam($group_name);
        if (!$target_group) {
            throw new Exception("グループが見つかりません: {$group_name}");
        }
        
        try {
            // グループ権限に変更
            if (!posix_setegid($target_group['gid'])) {
                throw new Exception("権限の変更に失敗しました");
            }
            
            // ファイルの読み取り
            if (!is_readable($filepath)) {
                throw new Exception("ファイルの読み取り権限がありません");
            }
            
            $content = file_get_contents($filepath);
            
            return $content;
            
        } finally {
            // 必ず元の権限に戻す
            posix_setegid($original_egid);
        }
    }
    
    public function writeGroupFile($filepath, $content, $group_name) {
        $original_egid = posix_getegid();
        
        $target_group = posix_getgrnam($group_name);
        if (!$target_group) {
            throw new Exception("グループが見つかりません: {$group_name}");
        }
        
        try {
            // グループ権限に変更
            if (!posix_setegid($target_group['gid'])) {
                throw new Exception("権限の変更に失敗しました");
            }
            
            // ファイルの書き込み
            if (file_exists($filepath) && !is_writable($filepath)) {
                throw new Exception("ファイルの書き込み権限がありません");
            }
            
            $result = file_put_contents($filepath, $content);
            
            if ($result === false) {
                throw new Exception("ファイルの書き込みに失敗しました");
            }
            
            return $result;
            
        } finally {
            // 必ず元の権限に戻す
            posix_setegid($original_egid);
        }
    }
}

// 使用例
$fileAccess = new SecureFileAccess();

try {
    // www-dataグループの権限でファイルを読む
    $content = $fileAccess->readGroupFile(
        '/var/www/data/config.json',
        'www-data'
    );
    echo "ファイル読み取り成功\n";
    
    // データベースグループの権限でファイルに書き込む
    $result = $fileAccess->writeGroupFile(
        '/var/db/backup.sql',
        'BACKUP DATA',
        'database'
    );
    echo "ファイル書き込み成功\n";
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例4: グループベースのアクセス制御システム

<?php
class GroupAccessControl {
    private $access_rules = [];
    
    public function defineRule($resource, $allowed_groups) {
        $this->access_rules[$resource] = $allowed_groups;
    }
    
    public function checkAccess($resource) {
        if (!isset($this->access_rules[$resource])) {
            return false;
        }
        
        $allowed_groups = $this->access_rules[$resource];
        $current_egid = posix_getegid();
        
        // 現在の実効グループが許可リストに含まれているか確認
        foreach ($allowed_groups as $group_name) {
            $group_info = posix_getgrnam($group_name);
            if ($group_info && $group_info['gid'] === $current_egid) {
                return true;
            }
        }
        
        // 補助グループもチェック
        $groups = posix_getgroups();
        foreach ($allowed_groups as $group_name) {
            $group_info = posix_getgrnam($group_name);
            if ($group_info && in_array($group_info['gid'], $groups)) {
                return true;
            }
        }
        
        return false;
    }
    
    public function executeWithGroup($group_name, callable $callback) {
        $group_info = posix_getgrnam($group_name);
        if (!$group_info) {
            throw new Exception("グループが見つかりません: {$group_name}");
        }
        
        $original_egid = posix_getegid();
        
        try {
            // グループ権限に変更
            if (!posix_setegid($group_info['gid'])) {
                throw new Exception("実効GIDの変更に失敗しました");
            }
            
            // コールバックを実行
            return $callback();
            
        } finally {
            // 元の権限に戻す
            posix_setegid($original_egid);
        }
    }
    
    public function accessResource($resource, $group_name, callable $action) {
        // ルールをチェック
        if (!isset($this->access_rules[$resource])) {
            throw new Exception("リソースのアクセスルールが定義されていません");
        }
        
        if (!in_array($group_name, $this->access_rules[$resource])) {
            throw new Exception("グループ '{$group_name}' はリソース '{$resource}' へのアクセス権限がありません");
        }
        
        // 指定されたグループの権限で実行
        return $this->executeWithGroup($group_name, $action);
    }
}

// 使用例
$acl = new GroupAccessControl();

// アクセスルールを定義
$acl->defineRule('/var/www/html', ['www-data', 'developers']);
$acl->defineRule('/var/db', ['database', 'admin']);
$acl->defineRule('/var/log', ['syslog', 'admin']);

try {
    // www-dataグループでWebファイルにアクセス
    $acl->accessResource('/var/www/html', 'www-data', function() {
        echo "Webファイルを処理中...\n";
        // ファイル操作
        return "処理完了";
    });
    
    // databaseグループでDBファイルにアクセス
    $acl->accessResource('/var/db', 'database', function() {
        echo "データベースファイルを処理中...\n";
        // データベース操作
        return "バックアップ完了";
    });
    
    // 権限のないアクセスを試みる(失敗する)
    $acl->accessResource('/var/db', 'www-data', function() {
        echo "この行は実行されません\n";
    });
    
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

例5: setgidビットのシミュレーション

<?php
class SetgidSimulator {
    public function demonstrateSetgid() {
        echo "=== setgidビットの動作説明 ===\n\n";
        
        // setgidビットとは
        echo "setgidビットが設定されたファイルを実行すると、\n";
        echo "実効GIDがファイルの所有グループに設定されます。\n\n";
        
        // 現在の状態
        $rgid = posix_getgid();
        $egid = posix_getegid();
        
        $rgroup = posix_getgrgid($rgid);
        $egroup = posix_getgrgid($egid);
        
        echo "現在の状態:\n";
        echo "  実GID: {$rgid} ({$rgroup['name']})\n";
        echo "  実効GID: {$egid} ({$egroup['name']})\n\n";
        
        // setgidビットのシミュレーション
        echo "setgidファイル実行をシミュレート:\n";
        
        // 例: /usr/bin/wall (通常setgidビットが設定されている)
        $wall_stat = @stat('/usr/bin/wall');
        if ($wall_stat) {
            $wall_gid = $wall_stat['gid'];
            $wall_group = posix_getgrgid($wall_gid);
            
            echo "  ファイル: /usr/bin/wall\n";
            echo "  所有グループ: {$wall_group['name']} (GID: {$wall_gid})\n";
            
            if (($wall_stat['mode'] & 02000) === 02000) {
                echo "  setgidビット: 設定されています\n";
                echo "\n";
                echo "  このファイルを実行すると:\n";
                echo "  実効GID → {$wall_gid} ({$wall_group['name']})\n";
            } else {
                echo "  setgidビット: 設定されていません\n";
            }
        }
    }
    
    public function createSetgidFile($filepath, $group_name, $content) {
        // ファイルを作成
        if (!file_put_contents($filepath, $content)) {
            throw new Exception("ファイルの作成に失敗しました");
        }
        
        // 実行可能にする
        chmod($filepath, 0750);
        
        // グループ所有権を設定
        $group_info = posix_getgrnam($group_name);
        if (!$group_info) {
            throw new Exception("グループが見つかりません: {$group_name}");
        }
        
        chgrp($filepath, $group_info['gid']);
        
        // setgidビットを設定
        chmod($filepath, 02750); // 2はsetgidビット
        
        echo "setgidファイルを作成しました:\n";
        echo "  パス: {$filepath}\n";
        echo "  グループ: {$group_name}\n";
        echo "  パーミッション: " . decoct(fileperms($filepath)) . "\n";
        
        return true;
    }
}

// 使用例
$simulator = new SetgidSimulator();
$simulator->demonstrateSetgid();

// setgidファイルの作成例(root権限が必要)
// $simulator->createSetgidFile('/tmp/test_setgid.sh', 'www-data', '#!/bin/bash\necho "Running with setgid"');
?>

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

権限の制限

<?php
// 一般ユーザーは自分が所属するグループにのみ変更可能
function canChangeToGroup($gid) {
    $groups = posix_getgroups();
    return in_array($gid, $groups);
}

$target_gid = 1001;

if (canChangeToGroup($target_gid)) {
    echo "グループ {$target_gid} への変更が可能です\n";
    posix_setegid($target_gid);
} else {
    echo "グループ {$target_gid} への変更権限がありません\n";
    echo "所属グループ: " . implode(', ', posix_getgroups()) . "\n";
}
?>

setgidとseteuidの順序

<?php
// 権限を降格する場合の正しい順序
function dropPrivileges($uid, $gid) {
    // 重要: 先にグループを変更してからユーザーを変更
    // ユーザーを先に変更すると、グループ変更の権限を失う
    
    // 1. まず実効GIDを変更
    if (!posix_setegid($gid)) {
        throw new Exception("実効GIDの変更に失敗");
    }
    
    // 2. 次に実グループIDを変更
    if (!posix_setgid($gid)) {
        throw new Exception("実GIDの変更に失敗");
    }
    
    // 3. 実効UIDを変更
    if (!posix_seteuid($uid)) {
        throw new Exception("実効UIDの変更に失敗");
    }
    
    // 4. 最後に実UIDを変更
    if (!posix_setuid($uid)) {
        throw new Exception("実UIDの変更に失敗");
    }
    
    return true;
}
?>

エラーハンドリング

<?php
function safeSetegid($gid) {
    $original_egid = posix_getegid();
    
    if (!posix_setegid($gid)) {
        $errno = posix_get_last_error();
        $error = posix_strerror($errno);
        
        // よくあるエラー
        switch ($errno) {
            case 1: // EPERM
                throw new Exception("権限がありません。グループ {$gid} への変更が許可されていません");
            case 22: // EINVAL
                throw new Exception("無効なグループID: {$gid}");
            default:
                throw new Exception("実効GID変更失敗: {$error} (errno: {$errno})");
        }
    }
    
    // 変更が成功したか確認
    $new_egid = posix_getegid();
    if ($new_egid !== $gid) {
        // ロールバック
        posix_setegid($original_egid);
        throw new Exception("実効GIDの設定に失敗しました(確認エラー)");
    }
    
    return true;
}

// 使用例
try {
    $target_gid = 33; // www-dataのGID
    safeSetegid($target_gid);
    echo "実効GIDを {$target_gid} に変更しました\n";
} catch (Exception $e) {
    echo "エラー: " . $e->getMessage() . "\n";
}
?>

Windows環境での制限

<?php
if (!function_exists('posix_setegid')) {
    die("posix_setegidはこの環境では使用できません\n" .
        "Unix/Linux/macOS環境で実行してください\n");
}

// クロスプラットフォーム対応
class PlatformGroupManager {
    public function setEffectiveGroup($gid) {
        if (function_exists('posix_setegid')) {
            return posix_setegid($gid);
        }
        
        // Windows環境では代替処理
        throw new Exception("この環境ではグループ変更がサポートされていません");
    }
}
?>

関連する便利な関数

グループID関連関数の完全セット

<?php
// グループID取得
$gid = posix_getgid();      // 実グループID
$egid = posix_getegid();    // 実効グループID
$groups = posix_getgroups(); // 補助グループリスト

// グループID設定
posix_setgid($gid);         // 実グループIDを設定
posix_setegid($egid);       // 実効グループIDを設定

// グループ情報取得
$info = posix_getgrgid($gid);     // GIDからグループ情報
$info = posix_getgrnam('www-data'); // 名前からグループ情報

// グループリストの初期化
posix_initgroups('username', $gid); // 補助グループを初期化

// ユーザーID関連(組み合わせて使用)
$uid = posix_getuid();      // 実ユーザーID
$euid = posix_geteuid();    // 実効ユーザーID
posix_setuid($uid);         // 実ユーザーIDを設定
posix_seteuid($euid);       // 実効ユーザーIDを設定
?>

実用的な権限管理クラス

<?php
class PrivilegeManager {
    private $saved_uid = null;
    private $saved_gid = null;
    private $saved_euid = null;
    private $saved_egid = null;
    
    public function saveCurrentPrivileges() {
        $this->saved_uid = posix_getuid();
        $this->saved_gid = posix_getgid();
        $this->saved_euid = posix_geteuid();
        $this->saved_egid = posix_getegid();
    }
    
    public function restorePrivileges() {
        if ($this->saved_egid !== null) {
            posix_setegid($this->saved_egid);
        }
        if ($this->saved_euid !== null) {
            posix_seteuid($this->saved_euid);
        }
    }
    
    public function getCurrentPrivileges() {
        return [
            'uid' => posix_getuid(),
            'gid' => posix_getgid(),
            'euid' => posix_geteuid(),
            'egid' => posix_getegid(),
            'groups' => posix_getgroups(),
        ];
    }
    
    public function displayPrivileges() {
        $priv = $this->getCurrentPrivileges();
        
        $user = posix_getpwuid($priv['uid']);
        $group = posix_getgrgid($priv['gid']);
        $euser = posix_getpwuid($priv['euid']);
        $egroup = posix_getgrgid($priv['egid']);
        
        echo "=== 現在の権限情報 ===\n";
        echo "実UID: {$priv['uid']} ({$user['name']})\n";
        echo "実GID: {$priv['gid']} ({$group['name']})\n";
        echo "実効UID: {$priv['euid']} ({$euser['name']})\n";
        echo "実効GID: {$priv['egid']} ({$egroup['name']})\n";
        echo "補助グループ: " . implode(', ', $priv['groups']) . "\n";
    }
}

// 使用例
$pm = new PrivilegeManager();
$pm->displayPrivileges();
?>

まとめ

posix_setegid関数は以下の特徴があります。

✅ 実効グループIDを一時的に変更 ✅ ファイルアクセス権限の細かい制御 ✅ setgidビットの動作を理解するのに重要 ✅ セキュアなマルチユーザーシステムに必須 ✅ 権限の一時的な昇格/降格に使用

実効グループIDの制御は、セキュアなシステムプログラミングの基本です。適切に使用することで、最小権限の原則に従いながら、必要な時だけ特定のリソースにアクセスできるようになります。

ベストプラクティス

  1. 必ず元に戻す: try-finallyまたはデストラクタで元の権限に復帰
  2. 最小権限: 必要最小限の時間だけ権限を変更
  3. 順序を守る: 権限降格時はGID→UIDの順
  4. エラー処理: 権限変更の失敗を適切にハンドル
  5. ログ記録: セキュリティ監査のため権限変更を記録

セキュリティの考慮事項

  • 権限エスカレーション: 意図しない権限昇格を防ぐ
  • race condition: 権限変更とファイルアクセスのタイミング
  • 補助グループ: posix_initgroups()と組み合わせて使用
  • 監査ログ: 権限変更を記録して追跡可能に

安全な権限管理を! この記事が役に立ったら、ぜひシェアしてください。PHPのセキュリティとシステムプログラミングについて、他にも知りたいことがあればコメントで教えてくださいね。

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