本記事では、掲示板、X(Twitter)上で報告された下記の脆弱性について、経緯・脆弱性の内容を簡単にまとめるとともに、悪意ある攻撃を防ぐ方法について解説を行う。
本記事執筆の動機は、内容を理解するのに専門知識が必要だからか、誤った理解に基づく発言が散見されるためである。
VRChatのPublicワールドで「デスガン」と呼ばれる、故意によるワールドクラッシュが頻発されていたけれど、原因を韓国の方が見つけたようで韓国の掲示板で共有されていました。
— pera 페라 (@peraperavrc) November 8, 2024
フレンドの韓国人の方が日本の人にも教えて欲しいのことだったので翻訳(GPT)をして画像にしてみました。 pic.twitter.com/Y8OJUGyq6y
なお、記事執筆時点(2024/11/15)において、本脆弱性は対応済みである。
経緯の概要(2024/11/15現在)
下記掲示板(以下、単に掲示板と呼称する)において、QVペンの脆弱性を突いてデスガン(同ワールドにJoinしているユーザーのVRChatクライアントをクラッシュさせるシステムの俗称)を作成した、という旨の投稿があった(より正確には、これまで行われてきたワールドクラッシュにはQVペンの脆弱性を突いたものもあったのではないか、と提起しているにすぎない。詳細は後述する)。
掲示板の投稿内容は、後日Xにも投稿され、日本人VRChatユーザーの多くが知るところとなる。
以上の投稿を受けて、QVペン側に脆弱性を解消する対策が投入された。下記リンクは、該当のCommitである。VRChatのPublicワールドで「デスガン」と呼ばれる、故意によるワールドクラッシュが頻発されていたけれど、原因を韓国の方が見つけたようで韓国の掲示板で共有されていました。
— pera 페라 (@peraperavrc) November 8, 2024
フレンドの韓国人の方が日本の人にも教えて欲しいのことだったので翻訳(GPT)をして画像にしてみました。 pic.twitter.com/Y8OJUGyq6y
脆弱性の内容
掲示板で投稿された脆弱性の内容とは、どのようなものだったのだろうか。解説するにあたって、まずはQVペン自体の仕組みについて触れておく必要がある。見ていこう。
QVペンで同期が必要な理由
QVペンの描画自体は、UnityのTrail Rendererで行っている。Trail Rendererとは、オブジェクトの軌跡を描画するときに用いられるコンポーネントである。ペンの先の軌跡を描画するようにしてあげれば、あたかも空中にペンを書いているかのような挙動を達成できる。
実際のQVペンには、前述の機能に加えて、描画内容を同期する仕組みが備わっている。原則として、VRChatのワールドはデフォルトではすべてローカルで動作する。ペンの軌跡についても例外ではなく、前述の機能だけだと、QVペンで書いた内容は書いた本人にしか表示されない。描画内容については、書いた本人以外でも見えなければQVペンの意味がないから、都度同期させる必要がある。次節では同期の仕組みについて詳しく見ていく。本脆弱性の理解につながる重要な点である。
QVペンの同期の仕組み
前節で述べた同期を行うには、各ユーザー間で値を共有する必要がある。VRChat上でそれを達成するためには、U#(VRChatワールドのギミックを作成する上で使用される独自の言語。C#ライクな言語である)上の変数にUdonSyncedという属性(Attribute)をつければよい。
QVペン内部のソースコードを見ると、次のように記述されている。_syncedData
は、描画内容の同期に使用されている変数である。
[UdonSynced]
private Vector3[] _syncedData;
こうすると、ワールド内にいる誰かが与えた_syncedDataの変更が、同ワールドにいるユーザー全員に反映される。
Vector3
型は、Unityに標準で定義されている型である。下記に主な定義を示す。名前の通り三次元ベクトルを表現する型である。_syncedData
は、Vector3
型の配列として宣言されている。
public struct Vector3 : IEquatable<Vector3>, IFormattable
{
//
// 概要:
// X component of the vector.
public float x;
//
// 概要:
// Y component of the vector.
public float y;
//
// 概要:
// Z component of the vector.
public float z;
//注:以降省略。
※単に変数の値を書き換えるだけでは足らず、追加の手順が必要だが、脆弱性の解説には不要であるため省略する。
描画内容の同期は、_syncedData
を用いて、次の手順で行われる。ここでは説明のために、QVペンを使って書いたユーザーをA、それ以外のユーザーをBとCとする。
- AがQVペンを使って描画を行う。
- Aのペンの描画が終わったら、
_syncedData
にデータを格納する。格納するデータは、大きく分けて以下の二つで構成される。- 描画した各点の情報
- 各種メタ情報(いわゆるフッター。ペンに関する情報のほか、フッター自身のサイズも格納されている)
- B,CのVRChatクライアントは、Aから送信された
_syncedData
をもとに、各々のワールドにAが書いた描画内容を再描画する。
※EraseやClearの動作も同期しなければならないため、上記の手順だけでは足らない。説明のためにかなり簡略化している。
脆弱性の内容
脆弱性の内容を理解するのに必要な前提知識は揃った。本題に入ろう。
下記ソースコードは、QvPen_Pen.csに定義されているCreateInkInstance
メソッドのうち、脆弱性に関係のある三行を抜粋したものである。
//筆者注: 本説明に限って、data=_syncedDataと考えて差し支えない。
private void CreateInkInstance(Vector3[] data)
{
// 略
var line = lineInstance.GetComponent<LineRenderer>();
line.positionCount = data.Length - GetFooterLength(data);
line.SetPositions(data);
// 略
}
CreateInkInstance
メソッドは、前節で紹介した手順の三番が行われるときに必ず呼び出されるメソッドである。
LineRenderer
コンポーネントは、二つ以上の点の間に直線を引くために使用される。先ほど示した図の再描画のイメージを思い出してもらうと分かりやすい。
line.positionCount
には、描画する頂点数をセットする。先ほどの図で示した通り、data
には描画する点のほかに、フッターも含まれているのであった。よって描画する頂点数を算出するには、data
の要素数からフッターの要素数を引く必要がある。GetFooterLength
メソッドは、フッターに含まれているフッターのサイズを取得し返す役割を持っている。
参考までに、フッターのサイズをフッターに格納するコードを示しておく。下記のソースコードを見る限り、フッターサイズは3・4・0のいずれかしか想定されていない。
//QvPen.cs PackDataメソッドより。
SetData(data, FOOTER_ELEMENT_DATA_INFO, new Vector3(localPlayerId, mode, GetFooterSize(mode)));
public const int FOOTER_ELEMENT_DRAW_LENGTH = 3;
public const int FOOTER_ELEMENT_ERASE_LENGTH = 4;
//QvPen_Pen.cs
private int GetFooterSize(int mode)
{
switch (mode)
{
case MODE_DRAW: return FOOTER_ELEMENT_DRAW_LENGTH;
case MODE_ERASE: return FOOTER_ELEMENT_ERASE_LENGTH;
case MODE_UNKNOWN: return 0;
default: return 0;
}
}
掲示板で問題視されたのは、このフッターサイズに関してバリデーションチェック(入力値検証)が存在せず、任意の値を入れられるという点である。フッターサイズは、現実に存在するフッターサイズとは独立に存在し、チェック機構も存在しない。したがって、仮に本当のフッターサイズが3であったとしても、フッターサイズに100,10000,-10000といった値を入力できてしまうのである。
これのなにがまずいのか。掲示板で挙がっていた-10000000
をフッターサイズに入れた場合を考えてみる。line.positionCount
の計算式を思い出してほしい。
line.positionCount = data.Length - GetFooterLength(data);
GetFooterLength(data)
は-10000000
に置き換えられる。すると
line.positionCount = data.Length + 10000000
となる(-(-10000000)=10000000)。line.positionCount
の値がきわめて大きい値になってしまうのだ。
約1000万頂点分の処理(描画準備)を要求されたPCには、非常に大きな負荷がかかる。結果として、高負荷にさらされたVRChatクライアントは、クラッシュまたはフリーズに追い込まれる。
なお掲示板では1000万としているが、原理上int型の最大値までならいくらでも大きくできる。
本脆弱性を用いた攻撃の手順をまとめる。先ほど挙げた例ではAがペンを描画した人であったが、今回の説明では悪意を持ったユーザーとする。
- Aが何らかの手段を用いて、QVペン同期データ中のフッターサイズの値を不正に書き換え、
_syncedData
を同期させるようVRChat側に命令する。- VRChat,QVPen側からは、正規の同期要求と見分けがつかない。
- QVペン同期データを受け取ったB,CのVRChatクライアントは「Aが描画を行った」と判断し、Aの描画データの再描画を試みる(前節手順3に相当)。
- 不正な同期データにより、描画頂点数がきわめて大きな値に書き換えられる。
- B,CのVRChatクライアントは、ばく大な頂点数を処理しようとしてクラッシュしてしまう。
掲示板では、上記手順1をワールドギミックを用いて疑似的に再現することで、攻撃のデモンストレーションを行っているようである。デモンストレーションに使用されたソースコードが公開されていないため、詳細は不明である。
類似状況での再現実験
line.positionCount
にきわめて大きな値をセットしたとき、本当にVRChatがクラッシュもしくはフリーズするか、手持ちの環境で再現実験を行った。
実験は、QVペンを設置したローカルのテストワールド上で実施した。実験手順としては、line.positionCount
の値をソースコード上から直接書き換えた後、ワールドをビルド&実行してQVペンを描画する、というものになる。
掲示板と同じ状況での実験ではないが、ばく大な頂点数の描画処理が走る、という点では同じ状況といえる。異なるのは、処理のトリガーを自ら引くのか、外部から引くのかという点だけである。
9999995頂点(約1000万頂点)での実験結果
下記の動画は、それぞれVRChatの様子とメモリ・CPU使用率の推移を示している。フリーズが分かりやすいように、背景でキューブを回転させている。描画を行うたびにVRChatがフリーズし、メモリ使用率が跳ね上がっていることが分かる。
※録画機材の関係上、下記二動画は別撮りである。
※上記二回の動画とは、別に取得したもの。状況は頂点数以外全て同じ。
10億頂点での実験結果
10億頂点で実行した結果である。動画から分かるように、フリーズではすまず、VRChat自体がクラッシュしてしまった。
実験結果まとめ
以上の実験結果より、line.positionCount
にきわめて大きな値をセットした場合、VRChatがフリーズまたはクラッシュする可能性があることが実証された。
掲示板ではメモリの増大がクラッシュの要因と推察されているが、実際にはそれに加え、処理負荷の増大も大きな原因であると推察される。これは1000万頂点での実験結果で示したCPU使用率のグラフから明らかである。
攻撃可能性について
掲示板で示された情報だけで、実際に攻撃を行うのは極めて難しいだろう。本攻撃を成功させるためには、QVペンの同期データを不正に書き換える必要があるが、その手法は掲示板では示されていない。掲示板で示されたのはQVペンの脆弱性を突けば、原理的には攻撃可能なのではないかという事実だけである。本脆弱性を使用して、現実にワールドクラッシュを行えるかどうかについては、全く示されていない。さらに、現実にある「デスガン」がQVペンの脆弱性を突いているかどうかについては、可能性について軽く言及があるだけであり、その根拠は一切示されていない。
過去VRChatで起こった荒らし行為の例を踏まえるに、不正改造クライアントを併用する必要があると考えられる(同期データの書き換えは聞いたことがないが、本来呼び出せないはずの関数を不正に呼び出す事例ならX上で耳にしたことがある)。見た目のインパクトに反して、特筆すべき脅威となりうるかどうかは疑問符がつく。そもそも不正改造クライアントを導入できてしまうならば、QVペン以外にも攻撃対象はたくさんあるに違いない。
ともかく、本脆弱性の攻撃可能性は、QVペンの同期データを不正に書き換えられる手段を確立できるかという一点に集約される。
VRChatやUdonが悪いのか?
line.positionCount
にきわめて大きな値をセットしたときにVRChatがクラッシュしてしまうのが問題だ、だからVRChat,Udon側が対応すべきという意見が見受けられたため、本節で解説を加える。
まず大前提として、掲示板で提案された脆弱性はVRChatやUdonが原因の脆弱性ではない。QVペンシステムのインプットバリデーション漏れが原因である。
「いや、だからといって、line.positionCount
にきわめて大きな値を入れてクラッシュしてしまうのは問題だ、修正すべきだ」という反論があるかもしれない。確かに問題ではある……のだが、だからといってライブラリ側で対処することは困難である。
ライブラリは様々な目的で使用される。今回はたまたまVRChatという環境下だったが、もしかするとスーパーコンピューターという環境下でライブラリが使用されるかもしれない。該当環境下では、10億頂点という値は、異常値でもなんでもなく、通常用いられる値かもしれないのだ。
line.positionCount
にセットする値が、正常であるか、異常であるか。これを判断できるのは、ライブラリを使用するソフトウェアしかいない。本脆弱性であれば、QVペンシステムが該当する。
ただし本脆弱性が悪用されるときは、通常、不正改造クライアントが用いられると考えられる。その場合「VRChatに不正改造クライアントの侵入を許した」という部分については、基本的にはVRChat側に修正責務がある。
修正内容について
data
から取得するフッターサイズを0からdata.length
に制限することで、本脆弱性を解消している。詳細は、下記commit参照。
攻撃を防ぐために
ワールド制作者、アセット製作者、VRChat開発者以外の、一般のユーザーがとれる手段は多くはない。高負荷のアバターを見せてクラッシュさせる荒らしであれば、Safetyをかけて対策できるが、脆弱性を突いた攻撃であれば、一般のユーザーに出来ることはなにもない。せいぜい、VRChatを最新のバージョンにアップデートすることを心がけるくらいである。
本節では、悪意ある攻撃を防ぐ一つの考え方として、ソフトウェア開発者向けにセキュアコーディング(防御的プログラミング)の考え方について簡潔に取り上げる。全てを取り上げることは不可能であるため、詳細は、文末に取り上げた参考文献を参照してほしい。
いかに悪意ある攻撃を防ぐか
よくある勘違い
セキュアコーディングと聞くと、既知の脆弱性に対してどのように対処するかといった場当たり的な対応が思い浮かびがちである。しかし、本来のセキュアコーディングはそういうものではない。
SQLインジェクションを例として解説を行う。次のようなSQL文を考える。IDとパスワードを与え、認証を行う単純なSQL文である。
SELECT * FROM users WHERE username = 'user_input' AND password = 'pass_input';
このSQL文には脆弱性がある。なぜなら、user_input
に' OR 1=1 --
と入力した場合、AND AND password = 'pass_input';
がすべて無視されてしまい、パスワードがどんな値であっても、認証が通ってしまうからである。SQLインジェクションは有名な脆弱性であるため、既に知っている方も多いのではないだろうか。
SQLインジェクションの対策例として、よく挙げられるのが、特殊文字のエスケープである。上記の例であれば、シングルクォーテーションをエスケープすれば、攻撃は失敗する。他の攻撃に使われる特殊文字についても適宜エスケープすれば、攻撃は防げる。
……さて。本当に特殊文字のエスケープをするだけで、堅牢なソフトウェアになったのだろうか?
残念ながら、依然として脆弱なソフトウェアなままである。ざっと考えるだけでも二つ思い浮かぶ。
- もしuser_inputに100万文字を入力したら? SQLを受け取ったDBMSがクラッシュするかもしれない。
- もしuser_inputにエスケープシーケンスを回避するような文字列を入力したら?(壊れた文字エンコードを利用した攻撃手法が広く知られている)
二つ以外にも、きっと可能な攻撃は存在するだろう。
上記の例のように、既知の脆弱性に対する場当たり的な対処だけがセキュアコーディングとしてしまうと、考えるべき項目が膨大になってしまい、実際には穴だらけの対策となってしまう。
下記ページに掲載されている「Bonus Photograph」が、まさにこの状況を的確に例えている。闇雲に脆弱性を潰しても、簡単に回避されてしまうのだ。
We like the following photograph because it illustrates how the easiest way to break system security is often to circumvent it rather than defeat it (as is the case with most software vulnerabilities related to insecure coding practices).
訳:我々が以下の写真を好むのは、システムのセキュリティを破壊する最も簡単な方法は、大抵の場合、セキュリティを打ち破るのではなく、セキュリティの回避であることを示しているからである(安全でないコーディング手法に関連するソフトウェアの脆弱性のほとんどがそうである)。
より引用。
本来のセキュアコーディング
場当たり的な対策が意味をなさないことは前節で述べた。では、いったいどうすれば本当の意味で対策出来るのだろうか。
……これを真面目に語り始めると、それだけで本が一冊出来上がってしまうため、二つ端的に述べる。
「セキュアコーディングの目的は、攻撃されないコードを書くことではなく、間違いなく正しく動作するコードを書くことである。」
「まず信頼する領域を定めよ。領域外から来るすべてのデータを疑え。自らで検証したもののみを受け入れよ。検証に失敗したデータは絶対に受け入れようとしてはならない。そして、領域外へ送信するデータはその文脈に応じて全て無害化せよ。」
一番目については、書いてある通りである。ここでいう正しく動作するコードとは、仕様通りに動作するコードであることはもちろん、次の7要素を満たしたソフトウェアである。
- 機密性:許可した相手以外に情報が漏れることはないか。
- 完全性:情報は常に正確か。
- 可用性:使いたいときにシステムを使用できるか。
- 責任追跡性:システム内で誰が何を行ったのか説明できるか。
- 真正性:あるデータが、記述の通り確かに本物であるか。真正性を脅かす攻撃として、なりすましが挙げられる。
- 信頼性:ソフトウェアは一貫して期待通りに動作するか。
- 否認防止性:ユーザーによって行われた行為は、後から否認可能でないか。
信頼する領域を定めよ、というのは、信頼境界線(trust boundary) を引けという意味である。理想的には、他のシステムを一切信頼しないのが望ましいが、現実的には不可能である。例えば、OSを信用しないでプログラムを書くことは不可能である。なぜなら、ソフトウェアはOS上で動作するからである。
類似した考えは、ソフトウェアのセキュリティに限らず適用されている。現実世界でも、空港の保安検査場や会社のセキュリティゲート等で、信頼境界線が引かれている。検査場を通った者は全員信用できる者とみなすのである。
ネットワーク構築においても、同様の考え方は見られる。ファイヤーウォールや、VLANがそれである。
そして、信頼境界線の外からやってきたデータは信用できるシステム上で、すべて必ず検証しなければならない。 例外は存在しない。本手順は、入力検証(Input Validation) と呼ばれる。
入力検証における注意点をいくつか挙げる。
- 入力検証に失敗したデータは受け入れてはならない。無理やり処理して、受け入れようとしてはならない。不正なデータは出来る限り早い段階で取り除かなければならない。
- 不正なデータを受け入れてしまうと、脆弱性の温床となる。
- 入力検証は、形式的な項目(変数の型、特殊文字含有の有無等)に限らない。論理的な入力検証も存在する(例:あるユーザーのチケットの購入枚数が、現時点でのチケット購入可能枚数を超えているケース。論理的に考えて、そのような値はありえない)。
- 入力検証は可能な限りホワイトリスト形式で行うこと。ブラックリスト形式では、漏れが発生する可能性が高い。
入力検証に関しては、OWASP(Open Web Application Security Project)が発行しているSecure Coding Practices――Quick Reference Guideにも、次のように記載されている(一般にデータベースは、ソフトウェアの視点から見て信頼できるデータ源とはならない。間接SQLインジェクション攻撃が代表例として知られる)。
Conduct all data validation on a trusted system (e.g., The server)
訳:全データのバリデーションを信頼できるシステム上で実施すること(例:サーバー)。
Identify all data sources and classify them into trusted and untrusted. Validate all data from
untrusted sources (e.g., Databases, file streams, etc.)
訳:すべてのデータソースを信頼できるものと、そうでないものに識別すること。すべての信頼できないデータは検証すること(例:データベース、ファイルストリーム、等)
All validation failures should result in input rejection.
訳:(訳注:入力の)検証が失敗した場合、その入力は拒否されなければならない。
Validate all input against a "white" list of allowed characters, whenever possible.
訳:可能な限り、すべての入力の文字を許容されるホワイトリスト文字リストと照らし合わせて検証する。
入力検証はセキュリティ対策上、きわめて重要な手順である。最重要といっても過言ではない。
入力検証がないシステムは、鍵をかけず、ドアを開けっぱなしにした家と同じである。
入力検証については、論文:Seven pernicious kingdoms: a taxonomy of software security errorsにおいて、次のように触れられている。
Input Validation and Representation
Metacharacters, alternate encodings, and numeric representations cause input validation and representation problems. Of course, sometimes people just forget to do any input validation at all. If you do choose to do input validation, use a white list, not a black list.
Big problems result from putting too much trust in input: buffer over-flows, cross-site scripting attacks, SQL injection, cache poisoning, and basically all the low-hanging fruit the script kiddies love so much.
訳:
入力検証と表現
メタ文字、異なるエンコーディング、数偽表現は、入力検証と表現の問題を発生させる。入力検証を全く行わない人も中には存在する。もしあなたが入力検証を行うのであれば、ホワイトリストを使用すること。ブラックリストは使用してはならない。
入力を過度に信頼することは大きな問題を引き起こす。バッファオーバーフロー、クロスサイトスクリプティング、SQLインジェクション、キャッシュポイズニング、などスクリプトキディ(レベルの低いクラッカーを指す俗称)が大喜びするレベルの低い攻撃に利用されてしまう。
より引用。
領域外へ送信するデータは全て無害化せよ。 というのは、出力先で悪用されかねないようなデータ(例えば特殊文字)をエスケープするなどして、無害化せよ、という意味合いである。一般に、XSS対策として紹介されているような対策が、これに該当する。エスケープ出来ない場合、出力データに対しても検証を行うことを検討する。
CERTが発行したTop 10 Secure Coding ProcticesとOWASP発行のSecure Coding Practices――Quick Reference Guideには、データの無害化について、次のような記載がある。
Sanitize data sent to other systems. Sanitize all data passed to complex subsystems [C STR02-A] such as command shells, relational databases, and commercial off-the-shelf (COTS) components. Attackers may be able to invoke unused functionality in these components through the use of SQL, command, or other injection attacks. This is not necessarily an input validation problem because the complex subsystem being invoked does not understand the context in which the call is made. Because the calling process understands the context, it is responsible for sanitizing the data before invoking the subsystem.
訳:他のシステムに送信されるデータをサニタイズ(訳注:データの無害化)する。コマンドシェル、リレーショナルデータベース、市販の既製品 (COTS) コンポーネントなどの複雑なサブシステム [C STR02-A] に渡されるすべてのデータをサニタイズする。攻撃者は、SQL、コマンド、またはその他のインジェクション攻撃を使用して、これらのコンポーネントの未使用の機能を呼び出すことが可能である。呼び出される複雑なサブシステムは呼び出しが行われるコンテキストを理解していないため、これは必ずしも入力検証の問題ではない。呼び出しプロセスはコンテキストを理解しているため、サブシステムを呼び出す前にデータをサニタイズする責任がある。
Contextually output encode all data returned to the client that originated outside the application's trust boundary. HTML entity encoding is one example, but does not work in all cases.
訳:信頼境界線外のアプリケーションに返すすべてのデータに対し、文脈に応じた出力エンコードを行うこと。HTML要素エンコーディングはその一例ではあるが、すべてのケースで機能するわけではない。
Secure Coding Practices――Quick Reference Guideより引用。
出力データ無害化の具体的な手順は、データの出力先やデータが出力される状況によって異なる。文脈に応じた、と上記引用で述べられているのはそのためである。間違っても、サニタイズ用のAPIを呼び出せば終わり、と単純に考えてはならない。
噴水ワールドの同期システムでセキュアコーディングを考える
本章のまとめとして、下記に示す噴水ワールドの同期システムを例にとって、セキュアコーディングを考えてみる。
※実際には、ここで取り上げた例よりも、もっと、ずっと複雑である。あくまでも例ということを念頭において欲しい。
- ワールドには噴水が一つ存在する。
- 噴水の状態は、1:動作停止、2:噴水動作パターンA(水の色が赤になる)、3:噴水動作パターンB(水の色が青になる)、4:噴水動作パターンC(水の色が黄色になる)の四つである。同期変数
fountainState
で管理する。 - 噴水の状態は、ワールド内に配置したボタンによって変化する。ボタンは、ワールド内にJoinしている全ユーザーが操作可能である。
- 噴水の状態は、Globalである。噴水の状態がワールド全員に同期されることを意味する。
- 各VRChatユーザー上で動作している噴水ワールド同期システムは、同期変数
fountainState
を参照して、噴水の描画処理を行う。噴水の色情報は配列で抱え持っており(以下色情報配列と呼ぶ)、fountainState
から参照すべき色情報配列の添え字を計算している。
まず何が信用できて、何が信用できないのかを決めなければならない。UnityやUdonのライブラリは、無条件で信用できるものとしよう。それと、ソフトウェア自身も信用できるものとする。
では、同期変数fountainState
の値は信用できるだろうか。本記事で既に散々取り上げたように、明らかに信用できない。fountainState
の値は、いつ、だれが、どのように変更するのか、全く予想できないからである。よって、入力検証の対象になる。
仕様上、fountainState
の値は1,2,3,4のいずれかにしかならない。それ以外の値は、全て仕様上ありえない不正なデータである。したがって、それらの値のみを受け入れるようにコーディングすればよい。
こうすることで、攻撃者観点から見ると、攻撃の自由度が劇的に減る。不正な値を入力できる隙がないからである。
また、入力検証処理以後は、fountainState
の値が1,2,3,4の場合のみにおいて正常に動作するか考えればよくなるため、システムの開発者目線からしても、システムの見通しが非常に良くなる。セキュアコーディングは結果として、バグを減らすことにもつながるのである。
なお、間違っても「一旦値を受け入れて、処理をやっている途中でExceptionが起きたら、不正なデータが来たと判断して噴水の状態変更処理を行わない」のような実装をしてはならない。不正なデータは、不正なデータだと分かった段階で、処理を中断しなくてはならず、絶対に受け入れてはならない。例外処理はフェイルセーフ策としては有効だが、それを入力検証替わりにしてはならない。標準ライブラリに渡すことすら極力避けるべきである。もし標準ライブラリに脆弱性が存在したとしたらどうなるか考えてみて欲しい。
不正なデータは長く受け入れれば受け入れるだけ、攻撃者に攻撃させる隙を与える。
既に取り上げた以下の引用を思い出してほしい。
All validation failures should result in input rejection.
訳:(訳注:入力の)検証が失敗した場合、その入力は拒否されなければならない。
「同期変数は絶対に書き換えられないから大丈夫」「不正改造クライアントはEACで全部弾かれるから」だから何もしない。こう考えてしまうのも、間違いではないが、多層防御の考え方からすると不適切である。セキュリティ対策は、どこか一つが実施すればよいという類のものではなく、複数の層において実施すべきものである。そうしておけば、仮にどこか一つの層が突破されたとしても、別の層で攻撃は食い止められる。
大量の現金を保管するときに「金庫は絶対に破られないから」といって、現金が入った金庫を路上に置きっぱなしにする人はいないだろう。ソフトウェアのセキュリティ対策も同じである。
参考文献
暗号技術のすべて IPUSIRON著