Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Where Recursion Ends: Render Limits for Untrusted UI

2026年09月14日に「Dev.to」が公開したITニュース「Where Recursion Ends: Render Limits for Untrusted UI」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

信頼できないUIをレンダリングする際、データに潜む再帰的な無限ループがブラウザをクラッシュさせる。安易な対策では正常なUIも阻害するため、再帰ごとにデータ階層が深くなるかを厳しく検査し、さらに要素数などの制限を設けて安全性を確保する。

ITニュース解説

ウェブアプリケーション開発では、ユーザーからの入力や外部からのデータに基づいて、画面に表示されるUI(ユーザーインターフェース)を動的に生成する場面が多く存在する。しかし、もしそのデータが「信頼できない」もの、つまり悪意のある内容を含んでいる可能性がある場合、アプリケーションの安全性に重大なリスクが生じる。この記事では、特にUIの「再帰的な表示」が、どのようにしてアプリケーションのクラッシュやサービス妨害に繋がりうるのか、そしてそれを防ぐためにどのような技術的工夫が必要かを詳しく解説する。

たとえば、ウェブUIの構造をJSON形式で定義するシステムがあるとする。ここで、たった二つのUI要素"a"と"b"が、互いを子として参照し合うようなJSON定義を想像してみてほしい。具体的には、"a"が"b"の子であり、同時に"b"が"a"の子であるという構造だ。このJSONは文法的に全く問題なく、一見すると有効な定義に見える。しかし、UIを画面に描画するプログラム(レンダラー)が、この構造を「ツリー」(木のように枝分かれし、必ず末端を持つ構造)だと仮定して処理を進めると、"a"を描画し、その子である"b"を描画し、さらにその子である"a"を描画し、またその子である"b"を描画する、という無限ループに陥ってしまう。この無限ループにより、プログラムは使用できるメモリを使い果たし、スタックオーバーフローというエラーが発生して、最終的にウェブブラウザのタブがクラッシュしてしまうのだ。

このような無限ループを防ぐための「教科書的な」解決策として、サイクル検出という手法がある。これは、UI要素をレンダリングする際に、現在処理している要素が、過去に処理した自身の「祖先」要素の中に既に存在するかどうかをチェックし、もし存在すればその要素のレンダリングを拒否するという方法だ。この単純なサイクル検出を導入すれば、先ほどの"a"と"b"の無限ループは確かに防げる。しかし、この方法は、ウェブUIで非常に一般的に使われる「再帰的なツリー構造」まで壊してしまうという致命的な問題があった。例えば、コメントスレッド、ファイルブラウザ、またはネストされたメニューのようなUIは、「子ノード」がさらにその「子ノード」を表示するという、まさに再帰的な構造で表現される。これらのUIは、データがなくなるまで再帰を続けることで、正しい表示を実現するものであり、決して無限ループではないのだ。

そこで、より洗練された解決策として、「要素が自身の祖先に現れても、それが毎回異なるデータをレンダリングしているのであれば許可する」というルールが考えられた。この修正により、コメントスレッドのような正しい再帰構造は無事に描画できるようになり、一見すると問題が解決したように見えた。しかし、このルールはまた別の巧妙な悪意あるパターンを見落としていた。例えば、特定のデータセット(例として/itemsというパスで定義されたリスト)を、UIの各要素が繰り返し「絶対パス」で参照してしまうようなケースだ。この場合、レンダリングされる要素自体は毎回異なるリストのアイテムを参照するため、先のルールではサイクルとは見なされない。結果として、たった数行のJSON定義から、何万ものUI要素が生成され、ブラウザタブが極端に重くなり、実質的なサービス拒否攻撃(Denial of Service; DoS)を引き起こしてしまう。クラッシュはしないものの、ユーザーにとってはアプリケーションが停止したのと同じ状態になる。

結局、最終的に正しかったルールは「再帰する際に、データ構造のより深い階層にアクセスしているか?」というものだった。コメントスレッドのような正しい再帰では、レンダリングが進むたびに、例えば/tree/0から/tree/0/children/1のように、データのパスが常に深くなる。これに対して、無限ループやサービス拒否攻撃につながるパターンでは、パスが深くなることなく同じレベルや親のデータを参照し続ける特徴がある。この考えに基づき、システムは以下のチェックを行う。「現在レンダリングしようとしている要素が、その祖先と同じ要素である場合、現在参照しているデータのパスが祖先が参照していたパスよりも深く、かつ正確にその内部に位置しているか?」もしデータパスが深く潜っていない、つまり「進展がない」と判断された場合、その要素の再レンダリングは停止される。この厳密なチェックにより、正しい再帰は許容し、不正な無限ループや膨大な要素生成を防ぐことが可能になった。

システムが悪意のある再帰や過剰な要素生成のパターンを検出した場合、UIの定義全体を拒否してエラーにするのではなく、問題のあるその要素のレンダリングだけを停止し、警告メッセージをログに出力する。このアプローチには二つの重要な意図がある。一つは、ユーザー体験を損なわないためだ。たとえUI定義の一部に問題があっても、それ以外の部分が正しく表示されるのであれば、UI全体が真っ白になるよりはユーザーにとって望ましい。もう一つは、UIの定義データがネットワーク経由で「ストリーミング」されてくるような場合があるためだ。まだ完全にデータが届いていない状態で、一時的に未解決の参照があっても、それはまだ問題ではない。しかし、サイクルが完成してしまった場合は、完全にデータが届く前にアプリケーションがクラッシュしてしまう可能性があるため、その場で処理を中断する必要があるのだ。このサイクル検出は、開発者が設定を無効にした場合でも常に適用される。

無限ループや特定のサイクルによるサービス妨害は上記のサイクル検出で防げるが、悪意のない単に「巨大な」UI定義でも、リソースを過剰に消費する可能性がある。そのため、レンダリングには明示的な「予算」を設定することが不可欠だ。まず、「maxElements」は、UI全体の要素数の上限を定義する。これを超えると、セキュリティリスクを避けるため、UIは一切レンダリングされない。次に、「maxDepth」は、UIのネストの深さの上限を定義する。例えば、ボタンの中にボタン、さらにその中にボタン...と、無限に深くネストされたUI定義を制限し、上限を超えた部分は描画されない。最後に、「maxRepeatItems」は、リストのように繰り返し表示される要素の数を制限する。データが非常に長い場合でも、表示されるアイテム数を抑制することで、ブラウザのリソース消費を防ぐ。これらの制限は開発者がアプリケーションの要件に応じて個別に設定する必要があり、デフォルト値は設けられていない。

UI定義が大きくなると、その定義の正当性を「検証」する処理自体も大きな負荷となる。実は、UI定義の構造をチェックする検証プログラムも、内部で再帰的な処理を行うことがあるため、もしUI定義自体が非常に深いネスト構造を持っていたり、複雑なサイクルを含んでいたりすると、検証プログラム自身がスタックオーバーフローでクラッシュしてしまう危険性があった。つまり、安全性を確認するはずのチェックが、別の形でシステムを停止させてしまうのだ。この問題を解決するため、チェックの順序と効率性が極めて重要視された。まず、単純な要素数カウントを高速に実行し、次に反復処理(再帰を使わない方法)による深さ測定とサイクル検出を行う。これらをクリアした定義に対してのみ、より詳細な再帰的な構造チェックを実行する。また、同じ要素を何度もチェックしないよう、一度チェックした結果を記録する(メモ化)ことで、処理の効率も高められている。

初期の設計では、UI要素が安全上の理由でレンダリングを拒否された場合、画面には何も表示されないように制御されていた。これは一見正しい解決策に見える。しかし、UI要素は単に「表示される」だけでなく、「ある状態が変化したときに特定のアクションを実行する」という機能(「watch」と呼ばれる)を持つことがある。セキュリティレビューの過程で、レンダリングは停止されて見えなくなっているはずの要素が、裏では引き続き「watch」機能を使ってアクションをディスパッチし続けていることが判明したのだ。これは、見えないUIが悪意のある動作を継続する可能性を意味する。この問題を受けて、レンダリング拒否された要素は、表示だけでなく、その要素に紐づく全てのアクションも実行されないように、監視メカニズムのトップレベルで明確に制御されるように修正された。これにより、UIの「見た目」と「動作」の両方が確実に停止されるようになった。

信頼できないソースから提供されるUI定義において、安全な再帰の制限を実装することは、単に「制限値を設ける」という単純な話ではないことがわかる。その最も難しい部分は「何が正しい再帰で、何が悪意のある無限ループや過剰なリソース消費なのか」という定義の明確化だった。単純なサイクル検出では、ウェブアプリケーションで頻繁に使われる正常な再帰的ツリー構造まで壊してしまう。また、「異なるデータをレンダリングしていればOK」というルールも、データが深く潜らない再帰によってサービス拒否攻撃につながる可能性を見過ごしていた。最終的に、「再帰のたびにデータパスがより深く潜っているか」という厳密なルールが、安全と機能性の両立を実現した。この経験は、「JSONが構造的に有効であれば、そのUIは安全にレンダリングできる」という安易な考え方がいかに危険であるかを強く示している。記事冒頭のたった二つの要素からなるJSONの例は、文法的に完全に有効でありながら、無限ループを引き起こす危険性を内包していた。

関連コンテンツ

関連IT用語