[PHP]yaml_parse_urlとは?URLからYAMLストリームを直接読み込む方法とセキュリティ注意点を徹底解説

PHP

はじめに

これまでの記事で、YAMLファイルを読み込む yaml_parse_file() を解説しました。今回取り上げる yaml_parse_url() は、その名の通りURLを指定してYAMLデータを直接取得・解析するための関数です。

ローカルのファイルシステムだけでなく、http://https:// といったスキームを持つURLを指定することで、リモートサーバー上のYAMLデータをそのまま読み込むことができます。設定を外部サービスから取得したり、他システムが公開しているYAML形式のAPIレスポンスを扱ったりする際に便利な関数ですが、外部から取得したデータを解析するという性質上、unserialize() と同様のセキュリティ上の注意点を抱えている、実務上重要な関数でもあります。本記事では基本的な使い方から、公式ドキュメントでも明記されているセキュリティリスクへの対処法まで詳しく解説します。


関数概要

項目内容
関数名yaml_parse_url()
所属拡張yaml拡張(PECL、標準では無効)
シグネチャyaml_parse_url(string $url, int $pos = 0, int &$ndocs = null, array $callbacks = null): mixed
引数1$url"scheme://..." 形式のURL(PHPのストリームラッパーが対応する形式)
引数2$pos — 複数ドキュメントが含まれる場合の抽出位置(-1ですべて)
引数3$ndocs — 参照渡し。ストリーム内のドキュメント総数が格納される
引数4$callbacks — カスタムのデシリアライズ処理を指定するコールバック配列
戻り値解析結果のPHPの値。失敗時は false
対応バージョンyaml拡張0.4.0以降(PECL)
重要な警告!php/object タグを使ったノードで unserialize() が有効な場合、信頼できない入力の処理は危険(公式ドキュメントによる明記)

セキュリティリスクの全体像(イメージ図)

  外部URL上のYAMLデータ
  "https://untrusted.example.com/data.yaml"
              │
              ▼
        yaml_parse_url($url)
              │
   ┌──────────┴──────────────────────┐
   │ もしYAML内に以下のようなタグが       │
   │ 含まれていたら...                   │
   │                                     │
   │ !php/object 'O:8:"MyClass":...'    │
   └──────────┬──────────────────────┘
              ▼
  ┌───────────────────────────────┐
  │ yaml.decode_php が有効な場合、          │
  │ 内部的にunserialize()相当の処理が行われ  │
  │ PHPオブジェクトインジェクションの         │
  │ リスクが生じる可能性がある               │
  └───────────────────────────────┘

ポイントは、yaml_parse_url()自分の管理下にないリモートのデータを、URL経由でそのまま解析するという点です。信頼できないソースのURLを指定した場合、そのレスポンス内容によっては、unserialize() の記事で解説したのと同様のPHPオブジェクトインジェクションのリスクが生じます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicYamlUrlParser
{
    public function parse(string $url): mixed
    {
        if (!extension_loaded('yaml')) {
            throw new RuntimeException('yaml拡張がインストールされていません。');
        }

        return yaml_parse_url($url);
    }
}

$parser = new BasicYamlUrlParser();
// 自分が管理する信頼できるURLを指定する例
$data = $parser->parse('https://config.example.com/app-config.yaml');
print_r($data);

例2:yaml.decode_phpを無効化した安全な読み込みクラス

<?php

class SecureYamlUrlFetcher
{
    /**
     * php/objectタグの処理を無効化することで、
     * PHPオブジェクトインジェクションのリスクを排除する
     */
    public function fetchSecurely(string $url): mixed
    {
        // 実行前にini設定を確認・変更する(php.iniでの恒久設定が望ましい)
        $original = ini_get('yaml.decode_php');
        ini_set('yaml.decode_php', '0');

        try {
            return yaml_parse_url($url);
        } finally {
            ini_set('yaml.decode_php', $original);
        }
    }
}

$fetcher = new SecureYamlUrlFetcher();
$data = $fetcher->fetchSecurely('https://external-api.example.com/data.yaml');
print_r($data);

例3:信頼できるホストのホワイトリストを検証するクラス

<?php

class WhitelistedYamlUrlLoader
{
    private array $allowedHosts;

    public function __construct(array $allowedHosts)
    {
        $this->allowedHosts = $allowedHosts;
    }

    /**
     * URL取得前にホスト名を検証し、
     * 信頼できるホストからのみYAMLを取得する
     */
    public function load(string $url): mixed
    {
        $host = parse_url($url, PHP_URL_HOST);

        if (!in_array($host, $this->allowedHosts, true)) {
            throw new InvalidArgumentException("許可されていないホストです: {$host}");
        }

        return yaml_parse_url($url);
    }
}

$loader = new WhitelistedYamlUrlLoader(['trusted-config.example.com']);
try {
    print_r($loader->load('https://trusted-config.example.com/settings.yaml'));
} catch (InvalidArgumentException $e) {
    echo 'エラー: ' . $e->getMessage() . PHP_EOL;
}

例4:取得失敗時の戻り値を適切にハンドリングするクラス

<?php

class ResilientYamlUrlFetcher
{
    /**
     * ネットワークエラーや解析失敗時に
     * falseが返るケースを明示的にハンドリングする
     */
    public function fetchWithFallback(string $url, mixed $fallback = []): mixed
    {
        $result = @yaml_parse_url($url);

        if ($result === false) {
            error_log("YAML URL取得に失敗しました: {$url}");
            return $fallback;
        }

        return $result;
    }
}

$fetcher = new ResilientYamlUrlFetcher();
$config = $fetcher->fetchWithFallback('https://maybe-down.example.com/config.yaml', ['default' => true]);
print_r($config);

例5:複数ドキュメントを含むYAMLストリームをすべて取得するクラス

<?php

class MultiDocumentUrlReader
{
    /**
     * $posに-1を指定することで、
     * ストリーム内のすべてのドキュメントを一度に取得する
     */
    public function readAll(string $url): array
    {
        $ndocs = 0;
        $result = yaml_parse_url($url, -1, $ndocs);

        return is_array($result) ? $result : [];
    }
}

$reader = new MultiDocumentUrlReader();
$documents = $reader->readAll('https://k8s-manifests.example.com/all.yaml');
print_r($documents);

例6:ローカルファイルとリモートURLを統一的に扱うクラス

<?php

class UniversalYamlSourceLoader
{
    /**
     * yaml_parse_url()はローカルファイルパスも受け付けられるため、
     * ローカル/リモートを意識せず統一的に扱える設計例
     * (ただし用途を明確にするため、通常はyaml_parse_file()との使い分けが推奨される)
     */
    public function load(string $source): mixed
    {
        $isRemote = preg_match('#^https?://#', $source) === 1;

        if ($isRemote) {
            // リモートの場合は信頼できるソースかどうかの検証を必ず行う
            $this->validateRemoteSource($source);
        }

        return yaml_parse_url($source);
    }

    private function validateRemoteSource(string $url): void
    {
        $host = parse_url($url, PHP_URL_HOST);
        if (!str_ends_with($host, '.internal.example.com')) {
            throw new InvalidArgumentException('社内ドメイン以外のURLは許可されていません');
        }
    }
}

例7:CI/CDパイプラインで公開設定を取得する実践例

<?php

class RemoteFeatureFlagLoader
{
    /**
     * 社内の設定管理サービスが公開するYAML形式の
     * フィーチャーフラグ設定を取得する(内部ネットワーク限定を想定)
     */
    public function loadFeatureFlags(string $serviceUrl): array
    {
        ini_set('yaml.decode_php', '0'); // オブジェクトインジェクション対策

        $flags = yaml_parse_url($serviceUrl);

        if (!is_array($flags)) {
            throw new RuntimeException('フィーチャーフラグの形式が不正です');
        }

        return $flags;
    }
}

$loader = new RemoteFeatureFlagLoader();
// $flags = $loader->loadFeatureFlags('https://feature-flags.internal.example.com/flags.yaml');

関連関数との比較

関数役割yaml_parse_urlとの違い
yaml_parse_url()URLからYAMLストリームを読み込んで解析本記事の対象。リモートソースからの取得が前提
yaml_parse_file()ローカルファイルからYAMLを読み込んで解析ローカルファイルシステムを主な対象とする
yaml_parse()YAML形式の文字列を解析既に取得済みの文字列を対象とし、取得処理自体は行わない
file_get_contents() + yaml_parse()URLの内容を取得してから解析より明示的な2段階の処理であり、取得部分の細かい制御がしやすい
unserialize()シリアライズ文字列をPHPの値に復元フォーマットは異なるが、外部入力に対する同様のセキュリティ注意点を持つ

よくある落とし穴(注意点)

  1. 信頼できないURLをそのまま渡してはいけない 公式ドキュメントで明確に警告されている通り、!php/object タグを含むYAMLデータを、yaml.decode_php が有効な状態で処理すると、unserialize() と同様のPHPオブジェクトインジェクションのリスクが生じます。ユーザーが指定可能なURLや、信頼性の確認できない外部サービスのURLを無検証で渡すことは避けましょう(例2・例3を参照)。
  2. yaml.decode_php の設定を確認・制御する このini設定によって !php/object タグの扱いが変わります。外部ソースからのYAMLを扱う場合、この設定を無効化しておくことが安全側の基本方針です(例2・例7を参照)。
  3. allow_url_fopen設定に依存する yaml_parse_url() がリモートURLを扱えるかどうかは、PHPの allow_url_fopen ini設定にも依存します。この設定が無効な環境では、URLからの読み込みが期待通りに動作しない可能性があります。
  4. 戻り値falseとネットワークエラーの区別がつきにくい URLへのアクセス失敗、YAML構文エラー、あるいはYAML内の値としてのfalseなど、さまざまな理由でfalseが返る可能性があります。原因の切り分けが必要な場合は、error_log()と組み合わせたエラーハンドリングを検討しましょう(例4を参照)。
  5. タイムアウトやリトライの制御が組み込まれていない yaml_parse_url() 自体には、HTTPリクエストのタイムアウト時間を柔軟に制御するオプションがありません。厳密なタイムアウト管理が必要な場合は、file_get_contents() とストリームコンテキスト、あるいはcURLで明示的に取得してから yaml_parse() に渡す、という2段階のアプローチの方が制御しやすい場合があります。

まとめ

観点まとめ
何をする関数か指定したURLからYAML形式のデータストリームを取得し、解析してPHPの値に変換する
主な用途外部設定サービスからの設定取得、リモート公開されたYAMLデータの読み込み
最重要の注意点信頼できないURLの処理はPHPオブジェクトインジェクションのリスクを伴う(公式警告あり)
安全に使う方法yaml.decode_phpを無効化する、アクセス先URLをホワイトリストで検証する
その他の注意点allow_url_fopenへの依存、エラー原因の切り分けの難しさ、タイムアウト制御の欠如

yaml_parse_url() は、リモートのYAMLデータを手軽に取得できる便利な関数である一方、公式ドキュメントが明記する通り、信頼できない入力の処理には明確なセキュリティリスクが伴います。unserialize() の場合と同様、「信頼できるソースにのみ使う」「危険な機能(yaml.decode_php)を無効化する」という2つの原則を徹底し、安全にリモートYAMLデータを活用しましょう。

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