セキュリティ設計 - ha1t/php-gm-server GitHub Wiki

セキュリティ設計

本ページでは、PHP GM Server のセキュリティ実装と設計方針について説明します。このサーバーは Gemini Protocol クライアントからのアクセスに対して、パストラバーサル攻撃の防止、リクエスト長制限、TLS/SSL の必須化、自己署名証明書による Trust On First Use(TOFU)モデルの実装を通じて、堅牢なセキュリティを提供します。

パストラバーサル攻撃の防止

realpath による正規化

StaticFileHandler クラスは、ファイルシステムのパストラバーサル攻撃(../ を用いた上位ディレクトリへのアクセス)を防止するため、PHP の realpath() 関数を使用してパスを正規化しています。

ドキュメントルート自体が初期化時に realpath() で正規化され、その後のファイルアクセスでは、リクエストパスと組み合わせたパスが再度 realpath() で解決されます。これにより、シンボリックリンクを含む複雑なパス構造も完全に展開されます。

public function __construct(string $docRoot)
{
    $this->docRoot = realpath($docRoot) ?: $docRoot;
}

Sources: php-gm-server/src/StaticFileHandler.php:29

ドキュメントルート内チェック

ファイルハンドラーが実際のファイルを読み取る前に、解決されたパスがドキュメントルート内に存在することを確認します。以下の 2 つの条件のいずれかを満たす必要があります:

  1. 解決されたパスがドキュメントルートと完全に一致
  2. 解決されたパスがドキュメントルートの直下(DIRECTORY_SEPARATOR で区切られている)
$realPath = realpath($filePath);
if ($realPath === false) {
    return null;
}

if ($realPath !== $this->docRoot && !str_starts_with($realPath, $this->docRoot . DIRECTORY_SEPARATOR)) {
    return null;
}

Sources: php-gm-server/src/StaticFileHandler.php:43-50

この二重チェックにより、攻撃者が絶対パスやシンボリックリンクを利用してドキュメントルート外のファイルにアクセスすることは不可能になります。

パストラバーサル防止フロー

flowchart TD
    A[クライアントリクエスト<br/>パス: /some/path] -->|path パラメータ| B[StaticFileHandler::handle]
    B --> C["filePath = docRoot + path"]
    C --> D{ディレクトリか?}
    D -->|はい| E["filePath += /index.gmi"]
    D -->|いいえ| F["realpath を実行"]
    E --> F
    F --> G{realpath 成功?}
    G -->|失敗| H["null を返却"]
    G -->|成功| I["realPath を取得"]
    I --> J{docRoot内チェック}
    J -->|失敗| H
    J -->|成功| K{ファイル存在?}
    K -->|いいえ| H
    K -->|はい| L["ファイル内容 + MIME<br/>を返却"]
Loading

リクエスト長制限

最大リクエスト長:1024 バイト

Gemini Protocol の仕様により、各リクエストは URL を含む 1 行のテキストで、最大長は 1024 バイト(CRLF 含む)です。RequestParser クラスはこの制限を厳密に実装しています。

private const MAX_REQUEST_LENGTH = 1024;

public static function parse(string $raw): ?array
{
    $line = explode("\r\n", $raw, 2)[0];

    if ($line === '' || strlen($line) > self::MAX_REQUEST_LENGTH) {
        return null;
    }
    // ...
}

Sources: php-gm-server/src/RequestParser.php:9-20

GeminiServer でのバッファオーバーフロー検出

GeminiServer の接続ハンドラーは、受信したデータをバッファに蓄積しながら、バッファサイズが 1024 バイトを超えた場合、CRLF がまだ検出されていない場合に即座にエラーレスポンスを返して接続を切断します。

$conn->on('data', function (string $data) use ($conn, &$buffer) {
    $buffer .= $data;

    if (!str_contains($buffer, "\r\n")) {
        if (strlen($buffer) > 1024) {
            $conn->write(ResponseBuilder::badRequest('Request too long'));
            $conn->end();
        }
        return;
    }
    // ...
});

Sources: php-gm-server/src/GeminiServer.php:51-60

これにより、DoS 攻撃やバッファオーバーフロー試行から保護されます。

リクエスト処理フロー

flowchart TD
    A["クライアント接続<br/>TLS確立"] --> B["データ受信"]
    B --> C["バッファに追加"]
    C --> D{CRLF 検出?}
    D -->|いいえ| E{バッファ > 1024?}
    E -->|はい| F["59 Request too long<br/>接続終了"]
    E -->|いいえ| B
    D -->|はい| G["RequestParser::parse"]
    G --> H{パース成功?}
    H -->|失敗| I["59 Invalid request<br/>接続終了"]
    H -->|成功| J["StaticFileHandler::handle"]
    J --> K{ファイル発見?}
    K -->|いいえ| L["51 Not found<br/>接続終了"]
    K -->|はい| M["20 MIME<br/>ファイル内容<br/>接続終了"]
Loading

TLS/SSL 必須化

SecureServer による強制 TLS

GeminiServer は ReactPHP の SecureServer ラッパーを使用して、すべてのクライアント接続を TLS/SSL で暗号化しています。TcpServer 上に SecureServer を重ねることで、平文接続は許可されません。

$tcp = new TcpServer("{$this->host}:{$this->port}", $loop);
$server = new SecureServer($tcp, $loop, [
    'local_cert' => $this->certPath,
    'allow_self_signed' => true,
    'verify_peer' => false,
]);

Sources: php-gm-server/src/GeminiServer.php:35-40

TLS 設定の詳細

設定項目 説明
local_cert server.pem サーバー証明書と秘密鍵を含むファイルパス
allow_self_signed true 自己署名証明書を許可(開発・個人使用用)
verify_peer false クライアント証明書を要求しない

デフォルトポート

Gemini Protocol の公式ポートは 1965 です。サーバーはデフォルトでこのポートでリッスンします。

Sources: php-gm-server/bin/server.php:12

自己署名証明書と TOFU モデル

自動証明書生成

CertificateGenerator クラスは起動時に自動的に自己署名証明書を生成します。既存の証明書が存在しない場合のみ、新規生成が行われます。

public function generate(string $hostname): string
{
    $pemPath = $this->certDir . '/server.pem';

    if (file_exists($pemPath)) {
        return $pemPath;
    }

    // ... 証明書生成処理 ...
}

Sources: php-gm-server/src/CertificateGenerator.php:16-22

証明書仕様

項目 説明
キータイプ RSA 2048 ビット 中程度の暗号強度、生成速度とのバランス
有効期間 365 日(1 年) 定期的なセキュリティ確認のため
フォーマット PEM 秘密鍵と証明書を同一ファイル
ファイルパーミッション 0600 所有者のみアクセス可能
署名者 自己署名 CA による検証なし
Common Name hostname サーバーのホスト名(環境変数で指定)
$privateKey = openssl_pkey_new([
    'private_key_bits' => 2048,
    'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);

$dn = [
    'commonName' => $hostname,
];

$csr = openssl_csr_new($dn, $privateKey);
$cert = openssl_csr_sign($csr, null, $privateKey, 365);

Sources: php-gm-server/src/CertificateGenerator.php:28-38

Trust On First Use(TOFU)モデル

Gemini Protocol は TOFU (Trust On First Use) セキュリティモデルを採用しています。これは以下の特性を持つ方式です:

  1. 初回接続時の証明書フィンガープリント記録:クライアントは初めてサーバーに接続する際に、サーバー証明書のフィンガープリント(通常は SHA-256)をローカルに記録します。

  2. 以後の検証:同じサーバーへの後続接続では、受け取った証明書のフィンガープリントが記録されたものと一致することを確認します。

  3. 一致しない場合の警告:証明書が変更された場合(サーバー側での再生成、中間者攻撃など)、クライアントは警告を表示し、ユーザーに確認させます。

  4. CA 検証不要:自己署名証明書であっても、その継続性が保証されれば信頼できるという前提です。

この方式は個人サイトやホビープロジェクトに適しており、CA による証明書管理コストを削減できます。

証明書の永続性

sequenceDiagram
    participant Client as Geminiクライアント
    participant Server as PHP GM Server
    
    rect rgb(200, 220, 255)
    Note over Client,Server: 初回接続時
    Client->>Server: TLS接続要求
    Server-->>Client: 自己署名証明書<br/>fingerprint: ABC123...
    Client->>Client: フィンガープリント<br/>をローカル保存
    Client->>Client: ホストキー認証完了
    Client->>Server: Geminiリクエスト
    Server-->>Client: レスポンス
    end
    
    rect rgb(220, 255, 220)
    Note over Client,Server: 2回目以降の接続
    Client->>Server: TLS接続要求
    Server-->>Client: 同一証明書<br/>fingerprint: ABC123...
    Client->>Client: ローカル記録と比較
    alt フィンガープリント一致
        Client->>Client: 接続信頼済み
    else フィンガープリント不一致
        Client->>Client: ⚠️ 警告表示
        Client->>Client: ユーザー確認待機
    end
    end
Loading

Sources: php-gm-server/src/CertificateGenerator.php:46-47

セキュリティ上の考慮事項

実装された保護機構

脅威 対策 実装箇所
パストラバーサル攻撃 realpath + ドキュメントルート内チェック StaticFileHandler
バッファオーバーフロー リクエスト長制限(1024 バイト) RequestParser, GeminiServer
平文通信による盗聴 TLS/SSL 必須化 SecureServer
中間者攻撃 自己署名証明書 + TOFU モデル CertificateGenerator
不正フォーマット URL スキーム検証(gemini のみ) RequestParser
ファイルシステムエラー null チェック、例外キャッチ StaticFileHandler, GeminiServer

開発環境と本番環境

このサーバーは「個人サイト公開」と「PHP 勉強会での発表」を目的としており、以下の特性を持つ環境での運用を想定しています。

  • 信頼できるネットワーク環境:自宅の LAN、または個人・小規模団体のサーバー
  • 限定的なクライアント:既知のクライアント(Amfora、Lagrange など)
  • データ機密性の要件が低い:パブリックな情報が主体
  • 高可用性要件がない:ダウンタイムが許容される

これらの前提がない環境では、以下の追加対策を検討してください。

制限事項と推奨事項

項目 現状 推奨事項
クライアント認証 未実装 リバースプロキシや OS レベルのファイアウォール使用
アクセス制御 パス名に基づくのみ 認証機構の導入
ログ記録 コンソール出力のみ ファイル出力、ログ集約の検討
暗号スイート React/Socket デフォルト TLS 1.3 への制限検討(PHP/OpenSSL 設定が必要)
HSTS 相当機構 なし(不要) Gemini プロトコルは TLS 必須のため不要
レート制限 なし nginx リバースプロキシなど外部ツール推奨

エラーハンドリング

Gemini ステータスコード

セキュリティ関連のエラーレスポンスは、以下の Gemini Protocol ステータスコードで返却されます。

public static function badRequest(string $reason = 'Bad request'): string
{
    return "59 {$reason}\r\n";
}

public static function notFound(): string
{
    return "51 Not found\r\n";
}

Sources: php-gm-server/src/ResponseBuilder.php:14-22

ステータスコード 用途 具体例
59 Bad Request プロトコルエラー、セキュリティ違反 59 Request too long, 59 Invalid request
51 Not Found ファイル不在、パストラバーサル検出 51 Not found
20 Success 正常なファイル配信 20 text/gemini

接続エラー処理

接続エラーが発生した場合、コンソールにエラーメッセージが出力されるとともに、該当接続は自動的に終了されます。これにより、メモリリークや僵尸接続の発生を防いでいます。

$conn->on('error', function (\Throwable $e) {
    echo "Connection error: {$e->getMessage()}\n";
});

$server->on('error', function (\Throwable $e) {
    echo "Error: {$e->getMessage()}\n";
});

Sources: php-gm-server/src/GeminiServer.php:44-47, 85-87

セキュリティ設定フロー

flowchart TD
    A["サーバー起動<br/>bin/server.php"] --> B["環境変数取得"]
    B --> C["GEMINI_HOST<br/>GEMINI_PORT<br/>GEMINI_DOC_ROOT<br/>GEMINI_CERT_DIR<br/>GEMINI_HOSTNAME"]
    C --> D["CertificateGenerator<br/>初期化"]
    D --> E{証明書ファイル<br/>存在?}
    E -->|あり| F["既存ファイル使用"]
    E -->|なし| G["OpenSSL で<br/>新規生成"]
    G --> H["RSA 2048<br/>365日有効<br/>自己署名"]
    H --> F
    F --> I["StaticFileHandler<br/>初期化"]
    I --> J["docRoot を<br/>realpath 正規化"]
    J --> K["GeminiServer<br/>初期化"]
    K --> L["SecureServer<br/>TLS リッスン"]
    L --> M["ポート 1965<br/>で待機"]
Loading

Sources: php-gm-server/bin/server.php:11-23

実装の特徴

防御的プログラミング

すべてのパース・ファイルハンドリング処理は、失敗時に null を返却する設計です。これにより、エラーケースが明示的に処理され、デフォルトの "Not found" レスポンスが返却されます。

型安全性

PHP 8.1 の strict_types と型宣言により、型チェックの甘さによるセキュリティ問題を最小化しています。

<?php
declare(strict_types=1);

Sources: php-gm-server/src/StaticFileHandler.php:1-3

関連ページ

⚠️ **GitHub.com Fallback** ⚠️