マガジン

オンライン対戦のチート対策:サーバーで判定すべきもの

遊びをつくる

執筆にClaudeを使用しています。

途切れた小さな橋をつなぎ直し、途中のチェス盤へ戻るための道を整える情景。
記事のテーマをもとに制作したAI生成の写真風イメージ。

オンライン対戦ゲームを作るとき、「チートされたらどうしよう」という不安は多くの開発者が感じます。しかし対策の方向を間違えると、いくら実装を重ねても根本的な穴は塞がりません。クライアント側でどれだけ検証コードを書いても、攻撃者はブラウザの開発者ツールやパケット改ざんで簡単に迂回できます。 チート対策の核心は一言で言えます。「ゲームの結果に影響する判定はすべてサーバーで行う」です。クライアントは表示と入力の受け付けだけを担い、何が起きたかの最終判断はサーバーが持つ。この原則を守れば、クライアント側のコードを丸ごと書き換えられても、不正な結果をサーバーに承認させることはできません。 この記事では、何をサーバーに移すべきか・何をクライアントに残してよいかを具体的な基準で整理します。ゲームジャンルごとの典型的な穴と、ブラウザゲームに現実的な実装パターンも合わせて示します。

この記事の目次
  1. なぜクライアント検証は突破されるのか
  2. サーバーで持つべき状態と判定
  3. クライアントに残してよいもの
  4. ジャンル別の典型的な穴と対処
  5. 実装を始めるための判断基準
  6. 次の行動
  7. 参考資料

なぜクライアント検証は突破されるのか

ブラウザゲームはHTMLとJavaScriptで動きます。ユーザーはページを開いた時点でソースコードをすべて手元に持っています。難読化しても、時間をかければ読み解けます。

さらに問題なのは「通信の改ざん」です。プレイヤーが「移動した」「攻撃した」「アイテムを使った」という情報をサーバーへ送るとき、その通信内容をプロキシツールで書き換えられます。クライアント側でいくら「このアイテムは1個しか持っていない」と管理していても、サーバーへ届く「アイテムを100個使用」というリクエストは防げません。

JavaScriptの変数もメモリも、開発者ツールのコンソールから書き換えられます。player.hp = 99999 と実行すれば、体力を不正に増やせます。これをサーバーが検証しなければ、その状態のままゲームが進みます。

前提として押さえるべきことは「クライアントから来るデータは信頼しない」という姿勢です。これはセキュリティ全般に共通する考え方で、ゲーム固有の話ではありません。

サーバーで持つべき状態と判定

ゲームの「真の状態」はサーバーが持つ

対戦に関わるすべての状態変数はサーバーが正として管理します。具体的には次のようなものです。

  • 各プレイヤーのHP・MP・スコア・所持アイテム数
  • ターン数・残り時間・フェーズ
  • 盤面の状態(チェスの駒の位置、オセロの石の配置など)
  • 乱数のシード(後述)

クライアントはこれらの「コピー」を画面表示のために持ちますが、変更はサーバーに依頼し、サーバーが承認した結果だけを反映します。クライアントが「HP を 50 増やした」と主張しても、サーバーが持つ状態を変えない限り意味がありません。

アクションの合法性チェック

プレイヤーの行動をサーバーが受け取ったとき、それが「ルール上許可された行動か」を判定します。

  • 移動:チェスや将棋なら、その駒がそのマスに移動できるかをサーバーのルールエンジンで確認します。「ポーンが後退した」「ナイトが直線移動した」は拒否します。
  • 攻撃・スキル使用:クールタイム中でないか、MPが足りるか、射程内かをサーバーが持つ状態で確認します。クライアントが「MPは十分」と送ってきても、サーバーが持つMP値が0なら拒否します。
  • タイミング:「自分のターンでないのに行動した」「制限時間を超えた後に送信された」はサーバー側のタイムスタンプで検出します。

乱数はサーバーで生成する

攻撃のクリティカル判定、カードのドロー、アイテムのドロップなど、結果に影響する乱数はサーバーで生成します。クライアントが乱数を生成して「クリティカルが出た」と報告する構造は、常に有利な結果を送信できるため成立しません。

サーバーで生成した乱数の結果だけをクライアントに返します。「何が起きたか」はサーバーが決め、クライアントはその演出を表示するだけです。

クライアントに残してよいもの

すべてをサーバーに寄せるとレイテンシ(通信遅延)の問題が出ます。現実的な分担を考えます。

クライアントに残してよいのは、不正されても結果に影響しないものです。

  • キャラクターのアニメーション・エフェクトの種類
  • 音量・画質などのローカル設定
  • UI の状態(メニューの開閉、カメラ位置など)
  • 予測表示(後述)

「クライアント予測」はアクションゲームや格闘ゲームでよく使われる技法です。入力してからサーバーの応答が返るまでの数十ミリ秒、クライアントが「おそらくこうなる」と先に動かして見せます。サーバーの結果が戻ったとき、ずれていれば補正します。この予測計算はクライアントで行いますが、最終的な正解はサーバーが決めます。予測がずれても、チート行為とは関係がありません。

ジャンル別の典型的な穴と対処

ターン制ボードゲーム

ターン制は構造がシンプルなため、サーバー権威のモデルを比較的実装しやすいジャンルです。しかし見落としやすい穴が二つあります。

一つはタイムアウト処理です。「持ち時間を消費せずに手を指し続ける」ことを防ぐには、サーバー側でタイマーを持ち、超過したターンを自動的に終了させます。クライアントのタイマーは表示用です。

もう一つは試合放棄の検出です。接続が切れたのか意図的に放置しているのかを区別し、一定時間応答がなければサーバー側で敗北処理を確定させます。クライアントが「まだ考えています」と送り続けることで時間を稼げる構造は避けます。

スコアアタック型

シューティングやパズルで「スコアを送信する」形式の場合、スコアの値そのものをクライアントから受け取ってはいけません。代わりに「各フレームの入力ログ」をサーバーに送り、サーバーがゲームを再現してスコアを算出します。これをリプレイ検証と呼びます。

リプレイ検証はサーバー負荷が高いため、軽量なゲームや非リアルタイムのランキング集計に向いています。重いゲームでは全員に適用せず、上位スコアのみ検証するなど段階的に適用する方法もあります。

リアルタイムアクション

最も難度が高いジャンルです。毎フレームのサーバー検証は現実的でないため、「不自然な移動速度の検出」「位置のサーバー定期確認」「行動頻度の上限設定」を組み合わせて対応します。

たとえば、プレイヤーの移動速度がゲームの設計上の最大値を超えていたら、サーバーが最後の合法的な位置に戻します。この戻し処理はサーバーが持つ状態に基づいて行います。

実装を始めるための判断基準

設計段階で各データについて次の問いを立てます。

  1. この値が改ざんされたら、対戦の結果が変わるか? 変わるならサーバーで管理します。
  2. この計算の結果が偽りだった場合、相手プレイヤーが不利になるか? なるならサーバーで判定します。
  3. この値はクライアントだけで決めてよいか? 表示・演出・ローカル設定のみ「はい」です。

小規模なブラウザゲームの現実として、完全なサーバー権威の実装はインフラと開発工数が必要です。まず「スコアと勝敗の最終確定はサーバーが行う」「行動の合法性チェックをサーバーに持つ」の二点から始めると、最も悪質な改ざんを防げます。WebSocketを使ったリアルタイム通信では、Node.jsやDeno上に薄いゲームロジックのサーバーを立て、状態を持たせるのが現実的な出発点です。

次の行動

まず手元のゲームの状態変数を書き出し、「これが改ざんされたら試合結果が変わるか」を一つずつ確認してください。変わるものをサーバー管理に移す優先順位をつけることが、最初の設計ステップです。アソバースにゲームを投稿する際は、サーバー機能を持つ構成もZIPファイルでまとめてホスティング先URLを登録する形で公開できます。実装の疑問は公式ドキュメントや各言語のサーバーフレームワークのリファレンスで確認してください。

参考資料

  • OWASP(Open Web Application Security Project)― クライアント側検証の限界に関するガイドライン
  • Valve Developer Community Wiki ― Source Engineにおけるサーバー権威モデルの解説
  • MDN Web Docs ― WebSocket API リファレンス