読者です 読者をやめる 読者になる 読者になる

HTML5のシングルページアプリケーションのセキュリティ

昨日の記事で、抽象的なまま書きまくった反省もあるのだけど、それと同時に残念な気持ちになったので、すごく当たり前のことを書く。

オンラインゲームでクライアントに状態を持ったらメモリ操作されて危険っていうブコメが多かった。それは正直、古典的なウェブアーキテクチャから脱却できない残念な感じだと思ってる。

原則

クライアントはサーバーのキャッシュを作って、サーバーと同じロジックを持って、計算し、次の行動を決定する。

が、サーバーはクライアントから送られてくる情報を信用しない。 大事なことなのでもう一度言う。サーバーは、クライアントから送られてくる情報を信用しない。

対応

モデルの状態を受け取るのではなく、モデルのトランザクションを受け取る。

実装例

AからBに攻撃したい。この時にattackEnemyというAPIを作るとする。

ここでのクライアント目的では、手元のデータで対象に攻撃可能かどうかを判定するロジックを持っている。

  • A のHPが1以上
  • B のHPが1以上
  • A と B の距離が10未満

このとき直接モデルを操作するAPIを叩くのではなく、その手続きをAPI化する。

GET /api/attackEnemy

target_id: {target_id}

たぶんこんな感じになると思う。

クライアントでは、AとBの状態を判定し、以下の全てが満たされた時、「攻撃」ボタンが押せるようになるとする。 「攻撃」ボタンを押すと /api/attackEnemy?target_id=b がサーバーにリクエストされる。

危険性とバリデーション

ここでクライアントが攻撃される可能性がある。直接このAPIを叩かれた場合、クライアントの攻撃可能判定をすり抜けられる。

じゃあサーバーでどういうバリデーションを作るか。 クライアントと同じデータの、コピーではないモデルの実体を持つサーバーは、上の攻撃発動条件をまた計算する。クライアントで正常に計算されていた場合、もちろんサーバーでも通るから、リクエストは通る。

で、発動条件を満たしていない場合(だいたいは通信の処理時間の問題だが、チートの危険性もある)、モデルは変化せず、トランザクションをリジェクトする。プロダクションであり得ないデータ構造を送られた場合、危険なユーザーとしてサーバー側でチェックを入れるのもいいかもしれない。

クラサバで同一のロジックを二重に持っている分、冗長になっているのが欠点といえばそう。(Node.js使うとクラサバでコードを共通化できて嬉しいのがこの辺だったりする)

ダメな例

たぶん昨日の記事にセキュリティが〜と言ってる人は、クライアントとサーバーのモデルを計算し終えたあと、そのままサーバーと同期することを想定している気がする。そんなことをするわけがない。クライアントの計算結果や、バリデーションなど信用出来ない。おそろしくてできない。Game.userModel.set("gold",99999999999999) とかされたら一瞬でゲームの寿命が終わる。

セガのサムライアンドドラゴンズ、Vitaのネイティブだけど、全体的なゲームUIはリッチなのに、テキスト送りのたびにサーバーにデータを取りに行く、ケータイゲーによくある構成になってた。その部分が残念すぎて、自分は非常に萎えた。たぶんUIとサーバーのチームが別で、サーバーをソシャゲっぽいところに投げてたんだと思う。最初にマスターデータ取得して、apiバージョンが変わるまでクライアントに保存して使い回せばいい。

クッキークリッカーHTML5のシングルページアプリケーションのゲームだけど、あれはスコアとか簡単に書き換えられるけど、アプリケーションロジックの完全に静的データだけで、サーバーにリクエストとかしてない。ソーシャル要素がないから壊したところでユーザー本人にしか影響がない。飽きたらぶっ壊して遊べば良い。ソーシャルならそこらへんちゃんとやるべきという話。

自分が昔作ったWebSocketのMMOも、キー入力だけしか送っていなかった。アクションゲームとかだとキー入力だけでよいと思う。高FPS必須だけど。

なんでこんな記事を書いたかというと

UI操作のたびにおそらく不必要なリクエストが走って待たされるのが多すぎてイライラする。その操作にリクエスト必要ないだろって毎回思ってる。裏でこっそりロードしててほしい。jQuery.Deferredとか使ってそこらへん綺麗にかけると思う。 はてなブログのこのページの左上の管理画面とかもそう。