HN 日本語サマリー

← 一覧へ戻る
プログラミング

Psalm 7リリース:PHPセキュリティ&静的解析ソフトウェア

Psalm 7 release: PHP security&static analysis software (psalm.dev)

15 pointsby danogentili5 コメント

要約

PHPの静的解析ツールPsalmのバージョン7がリリースされました。デフォルトで有効になったセキュリティ解析機能は、パフォーマンスが10倍以上向上し、カバレッジも大幅に拡大しました。また、NoSQLインジェクションやLLMプロンプトインジェクションなど、より深いセキュリティ解析機能が追加されています。

全文翻訳

Psalm 7の発表

2026年11月2日 by Daniil Gentili - 10分で読む

Daniil Gentiliのブログ » より転載

Psalm 7のリリースを発表します!

以下のような、非常に多くの新機能とバグ修正が含まれています:

  • デフォルトで有効なセキュリティ解析: 型チェックとデッドコード検出と同じ実行で、パフォーマンスが10倍以上向上し、カバレッジが大幅に拡大しました。
  • より深いセキュリティ解析のテイント追跡: NoSQL、LLMプロンプトおよびURLインジェクション、php://inputやアップロードされたファイル名などの新しいソース、クロージャ、ジェネレータ、トレイト、参照渡しパラメータを流れるパス、Guzzle、PSR-7、Monolog、MongoDBなどのための既製のスタブ。
  • Hackの機能に基づいた新しい純粋性モデル: 関数が持つ可能性のある副作用を正確に宣言します(io、time、write-props...)。クラス、クロージャ、自動推論アノテーションのための純粋性テンプレートがあります。
  • グラフベースのデッドコード検出: クラス、関数、メソッド、定数、変数などが到達可能でない場合、Psalmはそれを検出します。未使用のサイクルも適切に処理します。
  • テンプレートデフォルト (@template TError of Throwable = RuntimeException) をクラス、関数、メソッドに適用: Result<int> が機能し、ライブラリはユーザーを壊すことなくテンプレートを追加できます。
  • PHP 8.5および8.6のサポート: 部分関数適用、パイプ演算子などの完全サポート。
  • 毎日のリリース。機能と修正は、着陸したその日に提供されます。dev-master を待つ必要はありません。
  • フレームワークプラグインを同梱。PHPUnit、Symfony、Laravel、Mockery、sabre/eventプラグインはPsalmと共に提供され、自動的に有効になります。セットアップ不要。
  • より多くのPsalmメンテナーチーム: @alies-dev がPsalmメンテナーチームに加わり、多くの新しい便利な機能とバグ修正に貢献しました!
  • 公式Discordコミュニティ!
  • その他多数の新機能とバグ修正!

Psalm 7は64ビットPHP 8.2から8.6で動作します。ほとんどのプロジェクトでは、Composer依存関係を更新するだけでアップグレードできます。詳細は以下のアップグレードを参照してください。

セキュリティ解析:より高速に、常にオン、単一実行で

テイントはビットマスクとして保存され、テイントグラフは各関数のコンパクトな要約で解決されます。これにより、セキュリティ解析が最大で10倍高速になります。

統合解析 (#11384) は、型チェック、セキュリティ解析、デッドコード検出を1回の実行でまとめて行います。そのため、テイント解析はデフォルトでオンになりました (#11399)。

テイントの問題は、SECURITY重大度で報告され、SARIFレポートではランク付けされます (#12068, #11383)。結果は実行間やスレッド数によって変化しなくなりました (#11946, #11952, #11953)。

より深いテイント追跡

<?php
$run = function (string $c): void {
    shell_exec($c);
};
$run((string) $_GET['cmd']); // TaintedShell, クロージャ経由

$mongo = new MongoDB\Driver\Manager('mongodb://localhost');
$mongo->executeQuery('db.users', new MongoDB\Driver\Query(['name' => $_GET['name']])); // TaintedNosql

/** @return mixed */
function body() {
    return json_decode((string) file_get_contents('php://input'), true);
}
eval((string) body()); // TaintedEval: php://input はソースです

psalm.dev で試す »

  • 新しいテイントの種類: TaintedNosql (MongoDBシンク)、TaintedLlmPrompt (プロンプトインジェクション)、TaintedUrlComponent / TaintedUrlPath (クエリおよびパスインジェクション)。curlオプションとCURLFileは、それらが何をするか(SSRF、ヘッダーインジェクション、ファイルアクセス、スリープ、コール可能オブジェクト)によって報告されます。全てのテイントタイプが文書化されました (#11944, #11746, #12097, #12099, #12468)。
  • 新しいソース: php://input、STDIN、$_SERVERおよび$_FILESのクライアント送信エントリ、getopt()、$argv、DNS応答、例外メッセージ、および読み取られる組み込み関数やオブジェクト (#11945, #12086, #12246, #12245, #12135, #12139)。
  • テイントが流れるパス: クロージャおよびコール可能パラメータ、ジェネレータおよびイテレータ、参照渡しパラメータ、参照渡しforeach、グローバルおよびスタティック変数、トレイトメソッド本体、昇格されたコンストラクタプロパティ、__toString()、継承されたメソッドとそのオーバーライド、例外、および組み込み関数とオブジェクト (#12147, #12096, #12095, #12121, #12243, #12255, #12272, #12091, #12127, #12262, #12084, #12102)。
  • 偽陽性の減少: 型が運べないテイントは削除されます(intはhtmlテイントを持ちません)。配列キーはより正確に追跡されます。推論された純粋関数は呼び出しごとに特殊化され、@psalm-cli-onlyはHTMLレスポンスではないコードをマークします (#12079, #12348, #12150, #12375, #12048, #12459)。
  • ライブラリスタブ: PsalmはGuzzle、PSR-7、PSR-18、psr/log、Monolog、mongodb/mongodb、amphp (http-client, byte-stream, file, process)、phpClickHouse、PhpSpreadsheet、Archive_Tar、php-vips、webklex/php-imap、webmozart/assertのテイントおよび純粋性スタブを提供します。スタブはコードがライブラリを使用するときにのみロードされます(loadLibraryStubs、デフォルトでオン) (#12460–#12467, #12481–#12484)。
  • より表現力豊かなアノテーション (ドキュメント):
    • 条件付きシンク: @psalm-taint-sink ($mode is 0 ? null : 'html') $content (#12458);
    • 配列値のみへのシンク: @psalm-taint-sink sql $params[*] (#12171);
    • 返り値の1つのキーの下にあるソース: @psalm-taint-source input return['key'] (#12455);
    • テイントの補集合: ~(html|has_quotes) (#12300);
    • @psalm-flow をコール可能オブジェクト(($compute()) -> return)、レシーバ(($this) -> return)、$thisプロパティ、キー付きストア(($value) -> Store::$data[$key])経由で (#12299, #12302, #12456, #12312, #12327);
    • @psalm-taint-property-bag (#12486);
    • psalm.xml で宣言されたカスタムテイント (#12330)。
  • テイント用のプラグインAPI: Codebase::getTaintNode(), getTaintPropertyNode(), addTaintPath(), getReceiverTaintPath()、および呼び出しの返り値、引数、レシーバのテイントノード (#12454, #12414, #12489, #12535)。

新しい純粋性モデル:ケイパビリティ

@psalm-pure, @psalm-mutation-free, @psalm-external-mutation-free は、Hackのコンテキストとケイパビリティをモデルにしたケイパビリティに置き換えられました。関数様は、それが持つ可能性のある副作用を正確に宣言します:read-props、write-this-props、write-props、read-globals、write-globals、write-refs、io、time。pureとimpureが両端にあります (#11984):

<?php
/** @psalm-capabilities time */
function today(): string {
    return date('Y-m-d'); // 問題なし
}

/** @psalm-pure */
function todayWrong(): string {
    return date('Y-m-d'); // ImpureFunctionCall: ... は time を必要とします
}

psalm.dev で試す »

  • Psalmは呼び出しグラフ全体で純粋性を推論します。アノテーションを追加できる場所では、提案できます:<psalm suggestPurityAnnotations="true"> を設定して MissingPureAnnotation および MissingImmutableAnnotation(およびそれらの抽象・インターフェースバリアント)を取得し、--alter --issues=MissingPureAnnotation,MissingImmutableAnnotation を実行してアノテーションを追加します。--alter コマンドは属性なしでも機能します。提案はオプトインです。
  • 純粋関数と読み取り専用関数は自動的にテイント特化されるため、コードにアノテーションを付けることでセキュリティ解析の偽陽性も削除されます。
  • 組み込み関数、SPL、Random、phpredis、MongoDB拡張機能などが、それらが何をするかを宣言します。
  • clone、__toString()、__invoke()、ArrayAccess、デストラクタ、new $class、パラメータデフォルトもチェックされるようになりました。
  • @psalm-pure はクラスに使用でき、プラグインは処理するメソッドのケイパビリティを宣言できます。

新しい純粋性モデルの記事でモデル全体を説明しています。

純粋性テンプレート

多くの関数は、与えられたものと同じくらいしか純粋ではありません:array_map() はコールバックが純粋な場合に純粋です。純粋性テンプレートは、型ではなくケイパビリティセットの値を持つテンプレートパラメータです(Hackの[ctx $f]コンテキストのようなもの)。関数は純粋かつ高階関数になることができ、各呼び出しのコストは引数がかかるものだけです (#11984)。

Closure[_] を使用すると、関数は与えられたクロージャから純粋性を取得します:

<?php
/**
 * @psalm-pure
 * @param Closure[_](int): int $f
 */
function apply(Closure $f, int $x): int {
    return $f($x);
}

/** @psalm-pure */
function double(int $x): int {
    return apply(fn(int $y): int => $y * 2, $x); // 問題なし: クロージャは純粋です
}

/** @psalm-pure */
function noisy(int $x): int {
    return apply(function (int $y): int { echo $y; return $y; }, $x); // ImpureFunctionCall: コンテキストは純粋ですが、applyへの関数呼び出しはioを必要とします
}

/** @psalm-pure */
function lengths(array $words): array {
    return array_map(fn(string $w): int => strlen($w), $words); // 問題なし: コールバックを受け取る組み込み関数は同じように機能します
}

クラス上では、純粋性テンプレートにより、各サブクラスがメソッドの許可する副作用を決定できます:

<?php
/** @psalm-purity-template P */
abstract class Handler {
    /**
     * @psalm-pure
     * @psalm-purity-from-template P
     */
    abstract public function handle(string $input): string;
}

/** @extends Handler[pure] */
final class Upper extends Handler {
    /** @psalm-pure */
    #[Override]
    public function handle(string $input): string {
        return strtoupper($input);
    }
}

/** @extends Handler[io] */
final class Printer extends Handler {
    /** @psalm-capabilities io */
    #[Override]
    public function handle(string $input): string {
        echo $input;
        return $input;
    }
}

/** @psalm-pure */
function viaUpper(Upper $handler): string {
    return $handler->handle('a'); // 問題なし: Upper は P を pure にバインドします
}

/** @psalm-pure */
function viaPrinter(Printer $handler): string {
    return $handler->handle('a'); // ImpureMethodCall: コンテキストは純粋ですが、Printer::handle メソッドは io を必要とします
}

Traversable、Iterator、IteratorAggregate、Generator は、それらを反復処理することが何をするかに基づいた純粋性テンプレートを持っています。iterableも同様です。純粋性アノテーションを持つジェネレータは、その本体が行うことにバインドします:

<?php
/**
 * @psalm-pure
 * @param Traversable[pure]<int, int> $xs
 */
function sum(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // 問題なし
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @param Traversable<int, int> $xs
 */
function sumAny(Traversable $xs): int {
    $total = 0;
    foreach ($xs as $x) { // ImpureMethodCall: コンテキストは純粋ですが、Traversable<int, int> の反復処理は impure を必要とします
        $total += $x;
    }
    return $total;
}

/**
 * @psalm-pure
 * @return Generator<int, int>
 */
function numbers(): Generator {
    yield 1;
    yield 2;
}

/** @psalm-pure */
function total(): int {
    return sum(numbers()); // 問題なし: numbers() は Generator[pure] を返します
}
  • 構文: 純粋性引数は、Hackのように型引数の前に角括弧で指定します:Closure[io](int): void、Handler[pure]、Iterator[pure]<int, string>、Box[io]<int>。名前付きテンプレートは@psalm-purity-template Pで宣言され、@psalm-purity-from-template Pで使用されます。
  • 境界とデフォルトはチェーンとして記述されます:@psalm-purity-template write-this-props <= C(write-this-props) <= write-this-props|write-props|io。メソッドは下限を無条件に使用できます。サブクラスは境界内に留まる必要があり、テンプレートをバインドしない場合はデフォルト値を取得します。
  • コンストラクタは、引数からクラステンプレートをバインドできます。Closure[P]パラメータや、__constructの条件付き@returnを使用します。例えば、new ArrayObject([...])はArrayObject[pure]なので、純粋な関数は作成したArrayObjectを埋めることができます (#12116, #12179, #12190)。
  • 純粋性テンプレートは、オーバーライド、トレイト、テンプレートスコープにネストされたクロージャ全体でチェックされます (#12015, #12126, #12027)。
  • 推論も可能: Psalmはテンプレート関数呼び出しの純粋性を推論し、--alterは関数が呼び出すクロージャパラメータにClosure[_]を追加します (#12077, #12019)。
  • コールバックやイテレータを受け取る組み込み関数とSPLは、純粋性テンプレートで宣言されています:array_map、usort、preg_replace_callback、iterator_to_array、CallbackFilterIterator、Fiberなど (#12122)。

純粋性テンプレートを純粋性モデルの記事で参照してください。

グラフベースのデッドコード検出

未使用コードは、参照のグラフ上で検出されるようになりました (#11939)。コードはエントリポイントから到達可能である場合にのみ使用されるとみなされるため、未使用コードのみで参照されているクラスやメソッド(サイクルを含む)が報告されます。作成されたがスローされない例外も報告されます (#12487):

<?php
final class Service {
    public function run(int $id): void {
        if ($id < 0) {
            new InvalidArgumentException('negative id'); // UnusedMethodCall: ... はスローまたは使用されません
        }
        echo $id;
    }
}

final class Ping {                                     // UnusedClass
    public static function ping(): void { Pong::pong(); }
}

final class Pong {                                     // UnusedClass
    public static function pong(): void { Ping::ping(); }
}

(new Service())->run(1);

psalm.dev で試す »

--find-unused-psalm-suppress は、クラスやTainted*問題に対する抑制もチェックするようになりました。

型システム

  • テンプレートデフォルト: クラス、インターフェース、トレイト、関数、メソッドに、デフォルト型 @template T = Default または @template T of Bound = Default を宣言できます。Psalmは型引数が省略された場合に使用します:@extends/@implements/@use内、最初の引数のみを持つジェネリック型(Result<int>)内、new、テンプレートを推論しない呼び出し内。ライブラリは、ユーザーが書いた全てのFoo<A>を壊すことなくテンプレートを追加でき、ユーザーはMissingTemplateParamを受け取らなくなります。テンプレートデフォルトを参照してください。

    <?php
    /**
     * @template TValue
     * @template TError of Throwable = RuntimeException
     */
    final class Result {
        /** @var TError|null */
        public ?Throwable $error = null;
    }
    
    /** @param Result<int> $result */
    function handle(Result $result): void {
        $error = $result->error; // RuntimeException|null (Psalm 6: MissingTemplateParam, Throwable|null)
    }
    
    /** @template T */
    interface Option {
        /**
         * @template TResult = T
         * @param (callable(T): TResult)|null $map
         * @return Option<TResult>
         */
        public function map(?callable $map = null): Option;
    }
    
    /** @param Option<int> $option */
    function check(Option $option): void {
        $same = $option->map(); // Option<int> (Psalm 6: Option<never>)
    }
    

    psalm.dev で試す »

  • 新しい型と構文:decimal-int-string、non-decimal-int-string、グローバル定数ワイルドカード、テンプレートに依存する型に対する条件付き型、条件付き型でのis not、スカラー型と条件付き型の厳密な交差。

  • コンストラクタの@return型。

  • 値による比較を行うintマスクに対する新しいIntMaskComparison問題。

PHP 8.5および8.6

  • PsalmはPHP 8.6で動作し、PHP 8.6の呼び出しマップを持っています。

  • PHP 8.5のパイプ演算子とPHP 8.6の部分関数適用は完全に型付けされています:Psalmは各パイプラインステージの型と各部分適用クロージャのシグネチャを推論するため、前のステージに適合しないステージは報告されます:

    <?php
    function slug(string $title): string {
        return $title
            |> trim(...)
            |> strtolower(...)
            |> str_replace(' ', '-', ?); // 問題なし: 各ステージは型付けされています
    }
    
    function lowercaseWords(string $title): string {
        return $title |> explode(' ', ?) |> strtolower(...); // InvalidArgument: strtolower の引数1は string を期待しますが、non-empty-list<string> が提供されました
    }
    
    function wrongLength(string $title): string {
        return $title |> trim(...) |> strlen(...); // InvalidReturnStatement: 'int<0, max>' は 'string' と一致しません
    }
    

    psalm.dev で試す »

プラグイン

  • バージョン管理されたプラグインAPI: Psalmのバージョン番号はプラグインAPIを追跡しなくなりました。プラグインAPIへの破壊的変更は、マイナーリリースやパッチリリースを含む、あらゆるPsalmリリースで提供される可能性があります。 psalm/psalm-plugin-api メタパッケージがそれらを追跡します (#11739)。破壊的変更ごとにメジャーバージョンが引き上げられ、そのREADMEに変更がリストされます。プラグインは、それに対応するPsalmリリースのみをインストールするように、ケアット範囲(^0.4)でそれを要求する必要があります。
  • 同梱プラグイン: Psalmはpsalm/plugin-phpunit、plugin-mockery、plugin-symfony、plugin-laravel、plugin-sabre-eventを要求し、composer.jsonが対応するライブラリを使用している場合に、それらとwebmozart/assertプラグインを有効にします(loadLibraryPlugins、デフォルトでオン) (#12568, #12570, #12571)。
  • プラグインの問題はカスタムドキュメントURLを持つことができ、ERROR_LEVELを尊重します (#11735, #11736)。

CLIと設定

  • --init-ci は、composer.lockのPsalmバージョンにピン留めされた公式Dockerイメージを実行するGitHub Actionsワークフローを生成します。ドキュメントには、Psalm GitHub Actionと、セキュリティ解析結果をGitHubコードスキャンに送信するPsalm Security Scanアクションが含まれるようになりました。
  • 公式Dockerイメージはghcr.io/psalm/psalmになりました。
  • 古いcompact出力形式はtableにリネームされ、新しい真にコンパクトなcompact形式が追加されました。
  • <unsuppressibleIssues>は、リストされた問題を抑制不可能にします。
  • 高速な起動:スキャンが減り、単一ファイルスキャンは完全なプロジェクトファイルリストを構築しなくなりました。Composerのオートローダーはディープスキャンされなくなりました。
  • テイントグラフ解決を含む、より詳細な進捗出力と、短く実用的なCLIメッセージ。

アップグレード

ほとんどのプロジェクトでは、Composer依存関係を更新するだけでアップグレードできます:

composer require --dev vimeo/psalm:^7.0 -W

その他すべてはアップグレードガイドにあります。

ほとんどのプロジェクトで気づくであろうこと:

  • テイント解析がデフォルトで実行されます。オプトアウトするにはrunTaintAnalysis="false"を設定します。
  • <psalm suggestPurityAnnotations="true"> を介して有効にされた場合、アノテーション可能なコードに対して新しい MissingPureAnnotation/MissingImmutableAnnotation 問題が表示されます。これらは --alter --issues=MissingPureAnnotation,MissingImmutableAnnotation で修正できます。Psalmが追加するアノテーションは、セキュリティ解析と型推論の両方を改善します:
    • セキュリティの偽陽性の減少: Psalmは、純粋関数および読み取り専用メソッドの呼び出しのテイントを、呼び出しサイトごとに特殊化します。ある呼び出しで渡されたテイント付き入力は、他の全ての呼び出しの結果をテイントしなくなります。アノテーションがない場合、Psalmはオーバーライドできない関数やメソッドについてのみこれを推論できます。
    • より良い型推論: Psalmは、不変クラスのメソッドの結果を呼び出し間で記憶するため、$a->get() !== null ? $a->get() : '' はstringになります。@psalm-immutableがない場合、2回目の呼び出しは依然として?stringです。
    • チェックされた契約: 純粋なコードはアノテーション付き関数を呼び出すことができ、全てのオーバーライドと実装はアノテーションが許可するもの以上を行わないことがチェックされます。

感謝

Psalm 7に貢献してくれた全ての人々、そしてベータ版とリリース候補版をテストし、問題を報告してくれた全ての人々に感謝します!

完全な変更履歴: 6.20.3...7.0.0