正規表現で同期関数をタイムアウトさせる
2023年4月6日
0 分で読めますコンテナイメージのカスタムタグへの対応は、どれくらい難しいのでしょうか?実際にやってみると……かなり難しいものでした!私たちのチームは、カスタムベースイメージのサポートをSnyk Containerに追加する作業に取り組んでおり、次の課題に直面したからです。タグを解析して各要素を取り出し、ほかの類似タグと比較できるようにすることです。解決するのが楽しい問題だったので、最終的な解決策にたどり着くまでの過程をご紹介します!
正規表現で問題を解決?
私たちは、正規表現、特に名前付きグループ機能を使うことを考えました。たとえば、次の文字列にマッチさせたい場合、
次の正規表現を使えます(たまに正規表現の悪夢を見る方には、先にお詫びします)。
ここで危険信号 🚩 が点灯した方もいるでしょう。正規表現が危険な場合があるのは周知のとおりです。ユーザーが入力文字列と正規表現の両方を指定できるようにするのは、攻撃を招くようなものです!
ユーザーは、たとえば次のようなベースイメージを簡単に作成できます。
そして、次の文字列を渡すこともできます。
これにより、サービスはほかのすべてのリクエストを処理できなくなってしまいます!AppSecチームが、こんなことを許すはずがありません。
では、最初から考え直す必要があるのでしょうか?幸い、そうではありません!このような攻撃を防ぐために設計されたライブラリ(正規表現パーサーエンジン)があります。Googleが開発したRE2です。一部の正規表現機能には対応していませんが、そもそもそれらは使わないので問題ありません。
ここからが本番です!
私たちは、制御できないバグやソフトウェアサプライチェーン攻撃の可能性がある外部依存関係だけを、唯一の安全策にしたくありませんでした。そこで、処理に時間がかかりすぎる正規表現をタイムアウトさせる方法が必要でした。
Nodeはシングルスレッドなので、並行実行できないコードをタイムアウトさせるのは簡単ではありません。そこで考えた解決策がこちらです。
スレッド 🧵 です!
おそらく、まず思い浮かんだのはこれでしょう。私たちも同じでした。スレッドを停止できるだけでなく、メインのイベントループをブロックしないという利点もあります!
では、コードを書いてみましょう。
メインスレッド
ワーカースクリプト
簡単ですよね?では、コードを実行してみましょう……

うーん、このエラーは初めて見ました。MDNの説明を確認してみましょう。

また、may not contain native (C++ backed) objects
オブジェクトは参照渡しではなく、複製されることがわかりました。そして、RE2は関数(クラス)であり、C++ベースのオブジェクトへのバインディングにすぎないため、引数として渡すこともできません。
スレッド内でオブジェクトを作成し、正規表現を文字列として渡す方法もあります。しかし、ここでも危険信号 🚩 が点灯しました。RE2を呼び出すたびにmatchオブジェクトを再構築する必要があると、パフォーマンスに無視できない影響が出るかもしれません。
憶測するのはやめて、実際にテストしてみましょう。benchmark.jsを使いました。
パフォーマンスのベンチマーク
⚠️ このようなマイクロベンチマークを実行するのは簡単ではありません。V8の最適化は非常に高度なので、結果は参考程度に捉えてください。マイクロベンチマークについて解説した、こちらのYouTube動画がおすすめです
ベースラインとして、RE2.matchは1秒あたり50万回実行できます。
RegExpオブジェクトは複製可能なので、RE2の代わりにそのままスレッドに送って、パフォーマンスを確認できます。ワーカースレッド内でRegExp.matchを実行し、結果を待つ場合、1秒あたり5万回実行できることがわかりました。90%の低下です!🤩
スレッド内でRE2オブジェクトを作成すると、さらに悪化して1秒あたり1万7,000回となりました。パフォーマンスが97%低下します。😭
スレッドは最適な解決策ではないのかもしれません。幸い、別の方法があります!
vmモジュール
Nodeにはvmというモジュールがあり、スクリプトを別のコンテキストで実行できます。この方法の利点は、タイムアウト機能が組み込まれていることです。
サンプルコードはこちらです。
マッチを実行するたびにスクリプトとコンテキストを作成する必要はありません。クロージャを作成して返せばよいのです。
最終的なコード
もう一度ベンチマーク
ベンチマークで実行して、パフォーマンスを確認しましょう。vm内で次の処理を実行すると、許容できる水準の1秒あたり20万回を達成しました。👍
あっ、タイムアウトを設定するのを忘れていました。タイムアウトを100ミリ秒にして、もう一度ベンチマークを実行しましょう。
実際にタイムアウトすることはないので、大きな影響はないはずですが……
ううううううううう😭😭😭
実際には一度も発生しないタイムアウトを追加しただけなのに、スレッドを使う方法よりもパフォーマンスが悪化するのはなぜでしょう!?
Googleで検索してみると、GitHubのissueにたどり着き、そこからPRへ。さらに、ドキュメントページの下部にひっそりと記された注記を見つけました。
⚠️
breakOnSigintオプションまたはタイムアウトを使用すると、新しいイベントループが作成され、それに伴ってスレッドも起動されます。そのため、無視できないパフォーマンスのオーバーヘッドが発生します。
なんてことだ!
まとめ
ワーカースレッドはvmを使う方法よりも複雑です(コードが読みにくく、より多くのリソースを必要とし、利用する関数のシグネチャをすべてasyncに変更する必要があります)。一方で、パフォーマンスは同程度だったため、vmを使う方法を採用しました。将来、ボトルネックになった場合は最適化できます。
今回、この機能をどのように実装したかをご紹介しました。ぜひ、新しいカスタムベースイメージの推奨機能をお試しください。気に入ったら、Twitterで@snyksecに感想をお寄せください。
開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。



