Atom枯渇はミスではない。私たちのCVEの3分の1を占める
(erlef.org)- EEF CNA が公開したCVEの35.8%は、制御されていないリソース消費であり、BEAMエコシステムでは繰り返し発生するatom枯渇が大きな割合を占めている
- Atom枯渇 はサービス拒否脆弱性であり、atomはガベージコレクションされずグローバルテーブルに蓄積され、テーブルがいっぱいになるとVMがクラッシュする
- ユーザー入力 のように、取り得る値の集合が有限だと保証されないデータからatomを作ると、DoSの危険が生じる。URI schemeも例外ではない
- 危険は
binary_to_atom/1、String.to_atom/1のような明示的な呼び出しだけでなく、JSONキーのatomデコード や文字列補間ベースの動的生成にも存在する - 安全に扱うには、実行時の新規atom生成を避け、既知の値は 明示的なルックアップテーブル や
to_existing_atom系に制限し、リンターで点検すべきである
Atom枯渇が生み出すサービス拒否脆弱性
- EEF CNA が公開したCVEのうち 35.8% は制御されていないリソース消費であり、BEAMエコシステムでは繰り返し起きるatom枯渇問題が大きな割合を占めている {p:36}
- 現在の分布はEEF CNAの Common Weaknesses ページで確認できる
- Atom枯渇 はサービス拒否(DoS)脆弱性である
- Atomはガベージコレクションされない
- グローバルatomテーブルに保存される
- テーブルがいっぱいになるとVMがクラッシュする
- 有限ではない値、特に ユーザー入力 からatomを作ると、潜在的なDoSになる
- 危険は明白な呼び出しだけに限られない
- Erlangの
binary_to_atom/1、list_to_atom/1 - Elixirの
String.to_atom/1、List.to_atom/1
- Erlangの
- 目立ちにくい危険なパターンも存在する
- Erlangで補間によって動的にatomを生成する例:
% Erlang: 보간을 통한 동적 atom 생성 list_to_atom("field_" ++ UserInput) - ElixirでJSONキーをatomとしてデコードする例:
# Elixir: JSON을 atom 키로 디코딩 Jason.decode(json, keys: :atoms) - Elixirで補間によって動的にatomを生成する例:
# Elixir: 보간을 통한 동적 atom 생성 :"field_#{user_input}"
- Erlangで補間によって動的にatomを生成する例:
安全な処理方法と点検対象
- Atom枯渇脆弱性は単なる不注意ではなく、入力が制御されている、または有限だと仮定したコードでよく発生する
- URI scheme は代表的な例である
- 処理するschemeは数個しかないように感じられることがある
- 値が外部入力から来る場合、取り得る集合がもはや有限だと保証できない
- 入力からatomを作るコードは、取り得る値の集合が 有限で、既知であり、強制されている場合 でなければ安全ではない
- 最も安全なアプローチは、実行時に新しいatomを作らないことである
- 許可される値が既知なら、明示的なルックアップテーブル を使う方が安全である
% Erlang case Scheme of <<"http">> -> http; <<"https">> -> https; _ -> error end - ルックアップテーブルが実用的でない場合は、新しいatomを作らず既存のatomだけを使う変種を使うべきである
- これらの関数は新しいatomを生成せず、エラーを発生させる
% Erlang binary_to_existing_atom(Value) list_to_existing_atom(Value)# Elixir String.to_existing_atom(value) List.to_existing_atom(value) - リンターは、脆弱性につながる前に危険なパターンを見つけるのに役立つ
- ElixirプロジェクトではCredoの Credo.Check.Warning.UnsafeToAtom を有効化することを検討できる
- このチェックは
String.to_atom/1、List.to_atom/1、Module.concat/1,2、keys: :atomsを使うJason.decode/2の安全でない呼び出しを指摘する - このチェックはデフォルトでは無効になっている
- ErlangまたはElixirプロジェクトのメンテナーは、バイナリ、文字列、JSONキー、URIコンポーネント、ヘッダー、設定値からatomを生成するコードを検索すべきである
- この脆弱性カテゴリは、CVEになる前に修正しやすいタイプの1つである
- さらに詳しいガイダンスは、EEF Security Working Groupの atom枯渇防止ガイド にまとめられている
1件のコメント
Lobste.rs の意見
Ruby で
Symbolが ガベージコレクション対象になる前の状況に似ているように聞こえるタイトルの意味がわからない。これは明らかに footgun に見える
「Ruby にも Erlang の atom のような symbol があるのでは?」と思うなら、その理解自体は合っているが、Ruby は symbol をガベージコレクションする
しかも、デフォルトでは Erlang の atom が格納されるルックアップテーブルは最大 1,048,576 個までしか許容しない
フォームのようなユーザー入力から atom を動的に生成すると非常に危険で、ソフトウェアはサービス拒否攻撃にさらされる
ただ私の経験では、「footgun」自体がかなり広い表現なので、どちらにしてもこのタイトルの言い回しは不自然だ
何か根本的な部分の 設計や実装が悪い ように聞こえるので驚いた。ネット上でずっと称賛されてきた言語だけに、なおさら意外だ
そのテーブルに参照カウントを追加するとコストが高く、何十年も存在してきたコードのスケーリング特性が変わってしまう
atom の最大数はデフォルトで 100 万個で、VM 起動時に決まる
落とし穴ではあるが、避けるのが難しいわけではない。かなり昔からの推奨事項は「ユーザー入力から atom を作るな」だった
たとえば JSON をパースするなら、通常はキーを atom に変換しないか、既存の atom である場合にだけ変換する。そうすれば atom キーでパターンマッチでき、コードロード時にそれらの atom はすでに作られており、包括句は atom の代わりに文字列を受け取れる
Elixir ははるかに一般的なので、Erlang 開発者はこれを知っている可能性が高いが、Elixir 開発者は知らない可能性がある
個人的には、atom をそういう形で使うこと自体が変に感じられる。Erlang の atom をだいたい C の enum 型に近いものとして理解しているからだ
あるやり方で単語を入力すると、内部的に enum になる便利機能、というイメージだ
記事ではユーザー入力に触れているが、そもそもユーザー入力から新しい enum 型を作りたくなるユースケースがなぜあるのかわからない。用途はきわめて限定的に見える
横のコメントではパースの話をしているが、理想的には事前にわかっているデータ構造をパースするものではないのか? 何か見落としている気がする
その言語で symbol を単なる高速な等価比較以上の別型として持つ利点は、少なくとも 2 つある。symbol は atom、つまり文字のリスト状シーケンスではなく原子的な単位なので、複数の演算子が異なる扱いをする。また symbol は ベクトル化されており、単一型リスト内に高密度に格納できる
K と Q では、データベーステーブルの列をベクトル化された型で表現するのが非常に望ましい。局所性が良く、メモリ効率が高く、多くの演算子に高速経路があるからだ。ただし symbol テーブルの制約があるため、カーディナリティの高い列に symbol を使うときは注意が必要だ
既知のスキーマの JSON をパースするなら、symbol は辞書キーとして非常に優秀で、k2/k3 では事実上必須だ。しかし正体不明の JSON であれば、ユーザー入力から来ていてはいけない
一部の K 方言では symbol の長さを短く制限し、64 ビット値に詰めて運べるようにしている。汎用性を犠牲にする代わりに、symbol テーブル自体が不要になる
「制御された入力」と「制御されていない入力」の区別は、セキュリティでは
nullかどうかのようなものに感じるwebpack-plugin-less-cssが信頼できない CSS ファイルを受け取るとサービス拒否になる、のような項目を見ると CVE 疲れをかなり感じるそれでも、ここでもう少し良い境界の示し方があるとよいと思う。たとえば文字列連結を経たときに、どの安全性の性質が保存されるのかといった合成規則も、うまく扱えるとよい
そして HTTP POST で受け取ったものを大量に
SafeString扱いにしていたなら、それはある程度は本人の責任だ