
ゲームに鍵をかけたら自分も締め出された|Signed URLが403になる原因と直し方
ゲームに鍵をかけたら自分の鍵も壊れてた|Signed URLとURL.searchParamsの罠
「ゲームのアクセスを制限したい」——そう思ってセキュリティ実装をしたら、自分もアクセスできなくなりました。
しかも原因は、JavaScriptのごく普通の書き方でした。個人開発の釣りゲームアプリで踏んだ罠と解決策をまとめます。
そもそもSigned URLって何?
「Signed URL(署名付きURL)」は、期限付きの合言葉が埋め込まれたURLです。
通常のURLは誰でもアクセスできますが、Signed URLはURLの末尾に署名(デジタル的なハンコ)が付いていて、その署名が正しい場合だけアクセスを許可します。署名が少しでも変わると即403エラー(アクセス拒否)です。

マンチーは個人開発の釣りゲームに、AWSのCloudFront(コンテンツ配信サービス)を使ったSigned URLでアクセス制限をかけようとしていました。
やりたかったこと
ゲームを開いたまま放置されるとセッションが残り続けるので、「ハートビート(定期的な生存確認の信号)」を実装しました。
キャッシュ回避のため毎回URLにタイムスタンプを付け加える処理をJavaScriptで書きました。
const url = new URL(signedUrl);
url.searchParams.set('_hb', Date.now());
fetch(url.toString());`URL.searchParams`はURLのパラメータを操作できる便利な機能です。一見、何も問題なさそうに見えます。
結果:毎回403。自分も締め出された
動かしてみると、ハートビートのたびに403エラー。しかもゲームそのものにもアクセスできなくなりました。セキュリティを強化しようとして自爆です。
原因:URL.searchParamsが署名を壊していた
CloudFrontのSigned URLの署名にはBase64エンコードされた文字列(`+`や`/`などの記号を含む)が使われています。
`URL.searchParams`はURLを「正規化」するとき、これらの記号を`%2B`や`%2F`などに変換してしまいます。CloudFrontは「署名が変わった=不正アクセス」と判断して403を返していたのです。

流れを整理するとこうです。
Signed URLを生成(署名に`+`や`/`が含まれている)
`URL.searchParams.set()`を使う
`+`が`%2B`に勝手に変換される
CloudFrontが「署名が違う」と判断 → 403
解決策:文字列で直接連結する
// NG
const url = new URL(signedUrl);
url.searchParams.set('_hb', Date.now());
// OK
const url = signedUrl + '&_hb=' + Date.now();`URL.searchParams`を使わず文字列として直接連結するだけ。署名文字列に一切触れないので壊れません。

教訓
「便利だから使う」が裏目に出るのがAWS開発のあるあるです。Signed URLを扱うときは署名した後は文字列を絶対に触らない、これだけ覚えておけば同じ罠を避けられます。
【この記事が興味深かった方は、以下記事もおすすめです】
<おすすめ記事①>
URLを知らない人には存在すら見えない。招待制で、安全に試してもらう仕組みを自作した話はこちら。
<おすすめ記事②>
お試し公開は、軽い鍵でいい。JSパスコードで限定公開する最小構成を知りたい方はこちら。
<おすすめ記事③>
公式どおりにやっても動かない。Cognito + Google OAuthで詰まる3つの罠はこちら。