ゲームに鍵をかけたら自分の鍵も壊れてた|Signed URLとURL.searchParamsの罠

AWS・クラウド構築

「ゲームのアクセスを制限したい」——そう思ってセキュリティ実装をしたら、自分自身もゲームにアクセスできなくなりました。

しかも原因は、JavaScriptのごく普通の書き方にありました。マンチーが個人開発のゲームアプリで踏んだ罠と、その解決策をお伝えします。

📌 AI時事ネタ:生成AIアプリのアクセス制御が急務に

2026年に入り、個人開発のAIアプリ・ゲームアプリが増える一方で、無断利用・APIコスト爆増の被害も増えています。「誰でもアクセスできる状態で公開してしまい、APIコストが月数万円になった」という事故がX上でも散見されます。アクセス制御の実装は今や個人開発者にとって必須スキルになりつつあります。


そもそもSigned URLって何?

まず「Signed URL(署名付きURL)」について簡単に説明します。

通常のURLは誰でもアクセスできます。でも「特定の人だけ」「一定時間だけ」アクセスを許可したい場合があります。そこで使うのがSigned URLです。

イメージとしては「期限付きの合言葉が埋め込まれたURL」です。URLの末尾に署名(デジタル的なハンコ)が付いていて、その署名が正しい場合だけAWSのCloudFront(コンテンツ配信サービス)がアクセスを許可します。署名が少しでも変わると「不正なアクセス」と判断されて403エラー(アクセス拒否)が返ってきます。

マンチーは個人開発の釣りゲームアプリに、このSigned URLを使ったアクセス制限を実装しようとしていました。


やりたかったこと:ハートビートでゲームの生存確認

ゲームを開いたまま放置されると、セッションが残り続けてしまいます。それを防ぐために「ハートビート」という仕組みを実装しました。

ハートビートとは「定期的に生存確認の信号を送る」処理のことです。心臓の鼓動のように、一定間隔でサーバーに「まだここにいます」と知らせます。信号が途絶えたらセッションを切る、という仕組みです。

このハートビートのリクエストをSigned URLで送ろうとしました。さらにブラウザのキャッシュ(一度読み込んだデータを使い回す仕組み)に引っかからないよう、毎回URLにタイムスタンプ(時刻情報)を付け加えることにしました。

そのときに書いたJavaScriptがこちらです。

const url = new URL(signedUrl);
url.searchParams.set('_hb', Date.now());
fetch(url.toString());

URL.searchParamsはJavaScriptでURLのパラメータを簡単に操作できる便利な機能です。set('_hb', Date.now())でタイムスタンプを追加しています。一見、何も問題なさそうに見えます。


結果:毎回403。自分も締め出された

実装して動かしてみると、ハートビートのたびに403エラーが返ってきます。

「Signed URLの生成に問題があるのかな」「期限が切れているのかな」と色々試しましたが、どれも違いました。しかも、ゲームそのものにもアクセスできなくなってしまいました。セキュリティを強化しようとしたら、自分も締め出される羽目になったのです。


原因:URL.searchParamsが署名を壊していた

原因はSigned URLの署名に使われている文字にありました。

CloudFrontのSigned URLには、署名としてBase64エンコードされた文字列が含まれています。Base64とは、バイナリデータを文字列に変換する方式で、`+`や`/`や`=`などの記号が使われます。

ところがURL.searchParamsはURLを「正規化」する際に、これらの記号をパーセントエンコード(`+`→`%2B`、`/`→`%2F`など)に変換してしまいます。

CloudFrontは「署名が変わった=不正なアクセス」と判断するため、毎回403を返していたのです。

図にするとこういうことです。

  1. Signed URLを生成(署名に`+`や`/`が含まれている)
  2. URL.searchParams.set()を使う
  3. URLの`+`が`%2B`に勝手に変換される
  4. CloudFrontが「署名が違う」と判断 → 403

AWSのドキュメントにも「署名付きURLにクエリパラメータを追加する場合は、署名する前に行うこと」と明記されています。署名した後にURLを触ると、署名が無効になるのです。


解決策:URL.searchParamsを使わず文字列で直接連結する

解決策はシンプルです。URL.searchParamsを使わず、文字列として直接連結します。

// NG:URL.searchParamsを使う(署名が壊れる)
const url = new URL(signedUrl);
url.searchParams.set('_hb', Date.now());
fetch(url.toString());

// OK:文字列として直接連結する(署名が壊れない)
const url = signedUrl + '&_hb=' + Date.now();
fetch(url);

URL.searchParamsは便利な機能ですが、内部でURLを再エンコードするため、署名済みURLには使えません。文字列の直接連結であれば、既存の署名文字列には一切触れないため安全です。


まとめ:Signed URLを触るときの鉄則

今回の教訓をまとめます。

  • Signed URLに後からパラメータを追加するなら文字列連結でURL.searchParamsは使わない。
  • CloudFrontはURLの文字が1文字でも変わると403を返す。署名は繊細。
  • 403が出たらまずURLの文字列が変わっていないか確認する。エンコードの罠は気づきにくい。

「便利だから使う」が裏目に出るのがAWS開発のあるあるです。Signed URLを扱うときは、署名した後は文字列を絶対に触らない——これを覚えておけば同じ罠を避けられます。

セキュリティを強化しようとして自爆したマンチーの経験が、どなたかのお役に立てれば幸いです。


AWS関連の過去記事もどうぞ:

コメント

タイトルとURLをコピーしました