定期更新型ネットゲームのセキュリティ入門 ~開発者編~
※参加者向けはこちら
世は一大定期更新型ネットゲーム時代である。
前置きとして、エタ歴ウン年の私は、定期更新型ネットゲームの開発者および運営者をとても尊敬している。
この記事は、より安全に楽しくネットゲームを運営するためのものだ。セキュリティ対策はたしかに面倒くさい。時間も労力も資金も足りない個人開発において、後回しにしたくなる気持ちは察してあまりある。
けれど、重大なインシデントがあってからでは遅い。もしアカウント乗っ取りや、データベースへの不正アクセス、偽サイトへの誘導による被害などがあったとき、個人開発、個人運営では個人がすべて対処しなければならなくなる。さらに、もし悪質な行為や犯罪行為の踏み台にされた場合、損害賠償などの法的リスクもありうる。
幸い、セキュリティ対策は俺の屍を越えてゆけ先例が豊富であり、やることがほとんど決まっている。ここではベタな攻撃を想定した対策を、想定される実装難易度も併せてを記載する。すべて基本のキなので、ぜひご一読いただいて、ツボを押さえていただきたい。
私はソフトウェアテスト業界(セキュリティ業界ですらない)の隅っこにいるよわよわエンジニアだ。有識者の方々、ご意見ご指摘ありましたら、ぜひコメントをお願いします。お待ちしています。
1. 総当たり攻撃対策
片っ端からパスワードを試す、原始的かつ強力な攻撃だ。
1-1. パスワードは下限8~12桁以上にし、英数字記号を許容する(難易度★☆☆)
2008年の報告では、4桁のパスワードは、英数字記号まじりでも約9分で破られるとある。繰り返すがこれは2008年の報告なので、14年経った今ではさらに速く、複雑になっていると考えられる。
パスワードは桁が増えれば増えるほど、複雑であればあるほど強いので、パスワードの文字数に下限を設け、英数字記号を許容しよう。
1-2. 脆弱なパスワードを登録させない(難易度★☆☆)
いくらパスワードの桁数を増やしても「1111111111」はあっという間に破られてしまうだろう。最低限、下記のパスワードは拒否するとよい。
数字のみのパスワード
同じ文字のみのパスワード
さらにこだわりたければ、よくあるパスワードは弾くというテもある。たとえばパスワード管理ツールを提供する会社、NodePassが発表する、「Top 200most common passwords(よくあるパスワードトップ200)」は大いに参考になる。
国別で集計されているため、日本の結果を参考にしたい。
1-3. 古い配布CGIプログラムを使わない(難易度★☆☆)
2000年代の個人サイトによく見られた、古い配布CGIプログラムには、ログイン認証に限らずセキュリティが怪しいものが多い。
プログラムをよく読み込み、自分で改修できるなら良いが、そうでなければ避けるのが吉だろう。
1-4. 非公開のログインIDを設ける(難易度★★☆)
Enoではなく、非公開のログインIDを登録させる。要はパスワードが2つになり、かなり強固になる。大手サービスだと、Yahoo!が導入している。
ちなみに、一般的なIDと同じ扱い(プロフィールページに記載する、プロフィールページのURLにする等)をすると無意味になるので注意。それでもEnoよりは収集の手間がかかるので、多少マシかもしれないが……。
すでに作りこまれたシステムに導入するのは難しいが、新規に開発するなら検討してよいだろう。
1-5. ログイン試行回数制限を設ける(難易度★★☆)
一定時間の間に、一定回数ログインに失敗すると、そのユーザのログインを制限する手法だ。総当たり攻撃対策には効果てきめんで、大手サービスでもよく導入されている。
ただし、定期更新型ネットゲームではオススメしづらい。なぜならアカウントの復旧手段がないからだ。多くの定期更新型ネットゲームでは、メールアドレスなど、ユーザに連絡を取れる個人情報を収集していない。よって、総当たり攻撃や単なるイタズラ等で、故意にログインに失敗され続けたとき、ユーザ本人がログインできなくなってしまう。
ログイン制限時間を10分程度ぐらいに短めにするなど、緩和する手段はあるにはあるが、定期更新登録に支障をきたすなど、ゲームプレイに大きな影響を与える可能性がある。
ログイン試行回数制限を検討するなら、次項の認証サービス導入を考えたい。
1-6. 認証サービスを導入する(難易度★★★)
セキュリティ面でもっともオススメがコレだ。ログイン認証周りは実装が決まりきっているわりに対策が面倒くさいので、信頼される認証サービスに丸投げしてしまおう。
有名なサービスに、Auth0やFirebase Authenticationがある。
もちろんデメリットもあり、
有料
すでに作り込んであるゲームには導入しづらい
認証サービスやアクセストークンによる認証を勉強しないといけない
プライバシーポリシーを載せる必要がある
が挙げられる。
上記で紹介したような、大手認証サービスの価格は良心的だ。Auth0なら無料枠で7000アカウントまで使えるし、Firebase Authenticationも1日100アカウントぐらいなら無料で作らせてくれる。
ただし、アクセストークンによる認証になるため、追加の勉強は必要だ。また、認証サービスのドキュメント、時には英文ドキュメントも読み込む必要がある。
また、外部サービスとはいえ個人情報を集めることになる以上、個人情報保護法上の義務にのっとり、プライバシーポリシー(個人情報保護方針)を載せることになる。例として、定期更新型ネットゲームに業種が似ていて、ソーシャルログインのみのサービスでは、Cocoforiaのプライバシーポリシーが参考になりそうだった。他にも参考になりそうなプライバシーポリシーがあれば教えていただきたい。
しかしベタなEメール+パスワード認証から、大手サービスのセキュリティにまるごと乗っかるソーシャルログインまで、比較的お手軽に実装できるのも確かだ。セキュリティ最強にしたいな~と思うなら、検討する価値はある。
2. XSS対策
ほぼすべての定期更新型ネットゲームでは、ユーザが投稿した内容を表示する機能がある。フォームからJavaScriptのコードなどを投稿し、アクセスしたユーザのブラウザ上で実行させる攻撃を、XSS(クロスサイトスプリクティング)と呼ぶ。
XSSが成功すると、ほぼなんでもできるといっていい。たとえば悪意あるサイトへリダイレクトされたり、サイトの内外を破壊して、ゲームを成り立たせなくすることもできる。
超古典的な攻撃だが、現代でも対策され尽くしているとはいえず、IPAの2022年4月~6月の報告でも、脆弱性の届出12,023件のうち、実に58%がクロスサイトスプリクティングだ。
2-1. 投稿内容を無害化(サニタイズ)する(難易度★☆☆)
基本はコレ。タグや式として認識される文字列をエスケープ処理する。ほぼどのプログラミング言語にもサニタイズする関数があるはずなので、調べてみよう。たとえばPHPだとhtmlspecialchars関数やhtmlentities関数がある。
イチから実装する場合、無害化の漏れに気をつけよう。チャット等の本文はもちろん、ユーザ名やキャラクター名、スキル名やスキル発動時のセリフ、アイコン画像等のURLなど、あらゆる箇所をエスケープ処理する必要がある。
無害化のタイミングだが、HTML等に出力する直前に行うとよいだろう。文字列を受け取った直後にエスケープ処理すると、その文字列を使って行う処理に異常が発生する可能性がある。
2-2. 古い配布CGIプログラムを使わない(難易度★☆☆)
1-3に同じ。XSS対策がおろそかであるものも見受けられる。
2-3. Webアプリケーションフレームワークを使う(難易度★★★)
正直これが一番確実ではある。現代で主流のフレームワークには、基本的な攻撃への対策はほぼ備わっている。
たとえばPHPであればLaravelが代表的だろう。デフォルトで無害化機能がある。Node.jsではExpressのライブラリにexpress-validatorがある。
最大のデメリットは学習コストだろう。きちんと使いこなすには、もう一つ言語を学ぶぐらいの学習コストが必要になる。もちろん公式の英文ドキュメントも読まなければならない。
また、フレームワークの機能に則って開発する必要があるため、すでに作り込んであるゲームには導入しづらい。新たに開発するとき、検討すると良いだろう。
3. CSRF対策
意図しないリクエストをログイン中のユーザのブラウザから対象サイトに送信させる攻撃を、CSRF(クロスサイトリクエストフォージェリ)と呼ぶ。具体例はWikipediaが一番わかりやすかったので、一読するとよい。
CSRFが成功すると、ユーザが自身のアカウントで意図しない投稿や操作をされてしまう。たとえば悪意あるサイトのURLや、誹謗中傷などを投稿されたり、アイテムやアカウントの削除を勝手に行われたりする。
3-1. CookieのSameSite属性をLaxに設定する(難易度★☆☆)
SameSite属性はCookieの属性のひとつだ。Cookieを、発行元のサーバ以外のサーバ(厳密にいうとCookieに設定されたドメインと同じサーバ)に送信できるかどうか設定できる。
詳しく書くとそれだけで記事がひとつできるので、参考になりそうな記事を紹介する。
SameSite属性はNone、Lax、Strictの値を設定できるが、Laxでまず問題ないだろう。Cookieの属性だが、例えばPHPではsetcookie関数で設定できる。
ただ、SameSite属性は古いブラウザ(IEなど)では対応されない。また、Cookieを使った対策のため、そもそもCookieを使わないサイト(匿名BBSなど)では使えない。
特に定期更新型ネットゲームでは、古いブラウザからのアクセスがリスクになるだろう。新しいブラウザが使われつつあるとはいえ、現状では次項のトークン発行の併用を検討したい。
3-2. トークンを発行する(難易度★☆☆)
CSRF対策の古典的な方法で、現代でも有効だ。おおざっぱな流れを記すとこうだ。
セッションを発行
ランダムな文字列(トークン)を発行し、セッションに保存
フォームにhidden属性などで2.のトークンを埋め込む
フォームをこのように実装すれば、正規のフォームからユーザが送信したとき、Cookieとトークンがサーバに送られる。サーバ側でCookieのセッションIDと、そのセッションに保存したトークンと送られたトークンを照合し、正しければOKとなる。
もう少し具体的な解説は、「○○(任意の言語) CSRF トークン 対策」などで検索すると良いだろう。昔から有名な対策なので、記事も多い。たとえばPHPだと(やや古いが)下記の記事がある。
3-3. CookieにHttpOnly属性、Secure属性を設定する(難易度★☆☆)
またCookieの話だ。厳密にいうとHttpOnly属性とSecure属性はCSRF対策だけのものではないが、Cookieの属性の話としてここにまとめた。
CookieにHttpOnly属性を設定すると、CookieがJavaScriptのDocument.cookieAPIにアクセスできなくなる。ブラウザ上でJavaScriptに読ませたいときは使えないが、例えばセッションIDのCookieには設定すべきだろう。
また、CookieにSecure属性を設定すると、そのCookieはHTTPSの通信のときのみ送信するようになる。HTTPS通信自体の話は6-1に記載したが、Cookie側でも対策をほどこすべきだろう。中間者攻撃対策にもなる。
3-4. 古い配布CGIプログラムを使わない(難易度★☆☆)
1-3に同じ。特に個人配布のCGIを埋め込みたいときは、ソースコードを一読することをオススメする。そうでなければ避けよう。
3-5. Webアプリケーションフレームワークを使う(難易度★★★)
2-3に同じ。最近のフレームワークには、CSRF対策機能がほぼある。
4. SQLインジェクション対策
ほぼすべての定期更新型ネットゲームでは、ユーザが投稿した内容をデータベースに反映させている。フォームからクエリの断片などを投稿し、データベース(厳密には関係データベース=RDBだが、ここではデータベースと呼ぶ)の不正操作を試みる攻撃を、SQLインジェクションと呼ぶ。
データベースを不正操作されることで、非公開の情報を盗まれたり、データベースを勝手に削除されたり、アクセス制限をかけられることもある。
ピンと来ない方は、実際に脆弱性のあるサイトをローカルに作ってアクセスしてみるとよい。親切な例が下記のサイトにある。
4-1. クエリの組み立ては全てプレースホルダで実装する(難易度★☆☆)
クエリに値を直接埋め込まず、プレースホルダを使って値を渡そう。たとえばPHPだと、PDOクラスのprepareメソッドに実装されている。
たとえばログイン処理において、クエリに値を直接埋め込んでしまうと、
ID「1'or'1'='1';--」、passは空欄でユーザが送信
クエリがそのまま生成される(例:SELECT * FROM users WHERE id = '1'or'1'='1';-- AND pass = ?)
クエリが実行される
SQLインジェクション(例:ログインに成功する)
となるが、プレースホルダを使って渡すことで、
ID「1'or'1'='1';--」、passは空欄でユーザが送信
プレースホルダが設定された文(例:SELECT * FROM users WHERE id = ? AND pass = ?)がデータベースに送られ、構文が確定する
「?」で割り当てられた場所に、ユーザから送られた文字列が代入される
構文が確定しているので、「1'or'1'='1';--」というIDかつpassが空欄の検索をすることになり、ログインに失敗する
肝心なのは、ユーザから送られた文字列が代入される前に、構文が確定する点だ。3.で代入される文字列(例では「1'or'1'='1';--」)はクエリとして扱われないので、SQLインジェクションを防ぐことができる。
4-2. 古い配布CGIプログラムを使わない(難易度★☆☆)
1-3に同じ。SQLiteなど、サーバではなく、アプリケーションに組み込めるデータベースの採用例がある。もちろん、これに対してもSQLインジェクションは有効だ。
4-3. ORMを使う(難易度★★★)
ORM(O/Rマッパー)を使うと、データベースに対する操作を、ライブラリを用いて行えるようになる。自前でクエリを組み立てないので、SQLインジェクションへの強力な対策になる。
ただし、無条件に歓迎すればよいものではなく、以下のようなデメリットがある。
学習コスト、導入コストがかかる
複雑な構文は使いづらい
内部の挙動を把握しづらい。とんでもなく重くなるクエリが発行されることもある
また、ORM特有の問題もある。例えば「N+1問題」という有名な課題があり、対策を講じなければならない。これに限らず、ORMを使うならSQLの勉強は不要かというと真逆で、どのようなクエリが発行されているかある程度把握する必要がある。
とはいえ、ORMを使えばデータベース操作をPHPなどの言語のみで記述でき、オブジェクト指向で扱えるのはたしかに便利だ。VSCodeなどのテキストエディタにおいて、コード解析によるチェックも適切に働いてくれるようになる。
PHPだとLaravelにEloquentというORMがある。フレームワーク以外だと、Idiormという軽めのORMもあるようだ。「○○(任意の言語) ORM」で検索するといろいろ出るので、自分に合ったORMを探してみよう。
4-4. Webアプリケーションフレームワークを使う(難易度★★★)
2-3に同じ。Laravelのように、デフォルトでORMが備わっているフレームワークも多い。
5. セッションハイジャック
なんらかの手段でセッションを乗っ取り、ユーザになりすましてアクセスする攻撃をセッションハイジャックと呼ぶ。CSRFと同じく、完全にユーザからの通信として自由に操作できるようになるため、ユーザが意図しない操作を行われることになる。
5-1. セッションIDを推測困難にする(難易度★☆☆)
セッションIDの生成になんらかの規則性がある(連番など)と、攻撃者は有効なセッションIDを推測できてしまう。セッションIDはブラウザにCookieとして保存されており、アカウントさえ登録すれば確認できるため、なんらかの規則性があった場合、すぐに見破られてしまう。
現代よく使われるセッション管理システムでは、デフォルトで推測困難なセッションIDが発行されることが多い。たとえばPHPのsession_start関数がそうで、初期設定では32桁のランダムな文字列が生成される。
もし自分の使っているシステムがどう対応されているか心配なら、セッションIDをログ出力するなどして、一度確認すると良いだろう。
5-2. ログイン成功後に、新しくセッションを開始し、ログアウトにセッションを破棄する(難易度★☆☆)
セッションハイジャックの手法のひとつに、セッションフィクセーション(セッションID固定化)という攻撃がある。
攻撃者からユーザへ任意のセッションIDを送りつけられ、そのセッションIDでログインすることを強制させられる攻撃のことだ。詳しくは先ほどの引用元をお読みいただきたい。
この攻撃は、セッションIDをログインするたびに新規に生成、ログアウトときに破棄すればかんたんに防げる。うっかり忘れがちなのがログアウト時の破棄で、ログアウトしたつもりなのにログアウトできていない、となってしまう。忘れずにsession_destroyしよう。
5-3. 古い配布CGIプログラムを使わない(難易度★☆☆)
1-3に同じ。……もう飽きた? ただまあ、「掲示板 配布」とか検索すると、いまもけっこう古いサイトが出てくるから、容赦してほしい。
6. その他対策
6-1. 常時SSL/TLS対応する(難易度★☆☆)
HTTP接続時の盗聴リスクは以下「フリーWi-Fi、公衆Wi-FiでSSL/TLS非対応サイトにアクセスしない」のとおり。
常時SSL/TLSは現代ならほぼすべてのレンタルサーバーで対応している。たいてい無料で導入できるので、ぜひしよう。
6-2. シェルを利用するメソッドを使わない(難易度★☆☆)
OSコマンドインジェクションという攻撃がある。
たとえばPHPにはsystemという関数がある。この関数にはシェルのコマンドを渡せるが、ユーザが入力する値を渡したとき、悪意ある命令を実行される危険性がある。
XSSと同じくエスケープ処理すれば無効化できる。しかしシェルはシステムに大きな影響を与えるコマンドを実行できるため、利用自体を避けたほうがいい。シェルを利用しなければならないことは、少なくとも定期更新型ネットゲームにおいては、ほとんどないはずだ。
6-3. ディレクトリリスティングを無効にするか、各ディレクトリにindex.htmlを置く(難易度★☆☆)
古いサイトにアクセスし、URLの末尾を適当に削り、「/」を最後に足すと、ずらっとディレクトリが並ぶことがある。あれがディレクトリリスティングだ。ディレクトリリスティング自体は攻撃でもなんでもなく、Webサーバの機能だが、該当ディレクトリ以下の構造が丸見えなので、非公開ページやファイルなんかを発見されやすい。
ディレクトリリスティングはWebサーバの設定で無効化できる。Apacheならmod_autoindexだ。もしくは全てのディレクトリにindex.htmlを置けば、そのページが表示されるようになり、ディレクトリリスティングを防げる。
6-4. 管理者向けページは推測されづらいURLにする(難易度★☆☆)
「https://teiki_site/admin.php」に管理者向けページを設置していないだろうか? あまりに分かりやすい場所に置くと、格好の標的になってしまう。どうせ管理者しか見ないので、類推しづらい位置においてブックマークしておこう。
また、定期更新型ネットゲームではあまり見ないが、WordPressのような一般的なCMSを使っている場合、必ず管理者ページはデフォルトのURLから変えておこう。一般に出回っているシステムの場合、攻撃者はまずデフォルトのURLへアクセスするからだ。
また、もちろん管理者用認証パスワードは長くしておこう。長さは正義である。
おすすめ参考サイト
安全なウェブサイトの作り方(IPA)
とりあえずコレを読もう。みんな大好きIPA直伝の安全なウェブサイトの作り方である。情報がやや古めな感じは否めないが、基本は一通り押さえられている。特に「セキュリティ実装 チェックリスト」は、サイト公開前に一通り確認してみよう。
おすすめツール
OWASP Zed Attack Proxy(ZAP)
オープンソースの脆弱性検査ツールだ。あいにく筆者もまだ使ったことないが、自分のサイトの脆弱性を検査できる。
英文のみだが、親切なドキュメントもある。
注意点として、間違っても他人のサイトに使ってはいけない。不正アクセス禁止法に違反するおそれがある。必ず自分の管理下にあるサイトに実行すること。ローカルにサーバを立ち上げたり、テスト用のサーバを借りるのが良いだろう。
さいごに
セキュリティを強化して、楽しい定期更新型ネットゲーム開発ライフを!