32文字のはずの鍵が53文字で入っていた — 買っていない人が5日間「有料会員」に見えていた — 制作日誌 #35
推し活アプリで、課金していない利用者が有料プランとして扱われていた。原因はサーバーに入れた鍵の余計な空白と、「確認に失敗したら有料扱いにする」という自分の書いた一行だった。どちらに倒すかの判断を、数字と一緒に残す。
会員制のジムで、受付の機械が会員証を読めなくなったとする。機械の設定は「読めなかったら、とりあえず通す」。正規の会員は困らない。ただ、会員証を持っていない人も、同じように通れてしまう。しかも、警報は鳴らない。機械にとって「読めなかった」は、異常ではなく想定内の分岐だからだ。
自作の推し活アプリで5日間起きていたのは、これだった。
| 例え | 実際 |
|---|---|
| 受付の機械 | アプリのサーバー(裏側で動くプログラム) |
| 会員証を読む装置の鍵 | 課金の状態を問い合わせるための鍵 |
| 「読めなかったら通す」 | 「確認に失敗したら有料扱い」という分岐 |
| 会員証を持たない人 | 課金していない利用者 |
ジムと違う所もある。ジムなら入口で誰かが気づく。アプリでは、有料の画面が開くこと自体は何も不自然ではないので、外から見てもおかしさが分からない。
数字で見ると
先に、測った数字を並べておく。
| 項目 | 値 |
|---|---|
| 鍵の本来の長さ | 32文字 |
| 実際にサーバーに入っていた長さ | 53文字(途中に空白入り) |
| 課金確認の問い合わせへの返事 | 毎回「401」=鍵が違うので断る |
| 鍵を入れてから気づくまで | 5日(9月28日→10月3日) |
| 有料プランの値段 | 月300円 |
32文字のはずが53文字。21文字ぶん余計なものが混ざっていて、途中には空白が入っていた。
なぜ鍵が壊れたか
サーバーに秘密の値を入れる時は、専用の道具に値を流し込む。私は Windows の端末から、値を画面に出してそのまま道具へ渡す、というやり方をしていた。
この渡し方だと、見えない改行や空白が値の後ろや途中にくっつくことがある。画面上では鍵はきれいに見える。サーバー側も、入れた値の中身は二度と見せてくれない。秘密の値なので、それが正しい作りだ。結果として、壊れた鍵が入ったことに、入れた瞬間は誰も気づけない。
しかも、この鍵を入れたのは、有料プランを売り始める準備の最中だった。同じ日に、値段の違う商品をいくつも登録し、別のアプリの設定も並行して触っていた。鍵を入れる作業は、その中の1行にすぎなかった。入れた後に「この鍵で本当に問い合わせが通るか」を試す手順は、どこにも書いていなかった。
課金の管理を任せている外部のサービスは、壊れた鍵での問い合わせを、毎回きちんと断っていた。断り方も正しい。悪かったのは、断られた後のこちらの分岐だった。
分かれ道は1行だった
サーバーには、こういう判断が書いてあった。
- 課金の状態を問い合わせる
- 返事が来て「有料」なら、有料
- 返事が来て「無料」なら、無料
- 問い合わせ自体に失敗したら、有料
最後の1行は、私が意図して書いたものだ。理由は「お金を払ってくれた人を、こちらの不具合で締め出したくない」。外部サービスが一時的に落ちた時、払った人が急に無料に戻されたら、それは最悪の体験になる。そう考えて、失敗した時は甘い方に倒した。
この判断は、鍵が正しい前提では筋が通っている。外部サービスが落ちるのは年に数回、それも数分だろう。その間だけ全員が有料に見えても、被害は小さい。
ところが、鍵が壊れていると「失敗」が数分ではなく、ずっと続く。年に数回のはずの例外が、毎回の通常運転になる。甘い方に倒す分岐は、例外がまれだという前提の上でしか安全ではなかった。
なぜ5日も気づかなかったか
気づけなかった理由は3つ重なっていた。
1つ目。失敗を記録していなかった。記録していたのは、通信そのものが途切れたような「本当の異常」だけだった。外部サービスから「鍵が違う」という返事が正常に届くのは、プログラムにとっては異常ではない。だから、5日間ずっと断られ続けても、記録には何も残らなかった。
2つ目。私自身のアカウントは、開発者として最初から素通りになっていた。自分で有料画面を開いて確かめても、問い合わせの道をそもそも通らない。毎日アプリを触っていても、壊れた道を一度も踏まなかった。
3つ目。症状が「得をする」方向だった。有料の画面が使えなくなったら、誰かがすぐ声を上げる。使えてしまう分には、誰も困らない。
結局、ある利用者の扱いを調べた時に、課金の記録が無いのに有料として扱われていることが見つかった。そこから鍵の長さを測って、53文字だと分かった。
直したこと
| 何を | どう変えたか |
|---|---|
| 失敗した時の倒し方 | 有料扱い → 無料扱い。その代わり失敗を記録する |
| 記録する範囲 | 通信の途切れだけ → 「200(成功)以外の返事」と「鍵が無い」も |
| 鍵の点検 | 無し → 定期的に、サーバーが自分の鍵の長さ・空白の有無・問い合わせ結果を書き残す |
| 鍵の入れ方 | 画面経由で渡す → 改行を付けない書き方で流し込み、直後に本物の問い合わせで成功を確かめる |
鍵の点検で、中身そのものは書き残さない。残すのは「長さ」「空白が入っているか」「最後の問い合わせの返事」の3つだけにした。鍵は秘密の値なので、記録に中身を出せば、それ自体が新しい事故の種になる。長さと空白の有無なら、外に漏れても鍵の再現には使えない。それでいて、今回の壊れ方は、この2つを見れば一目で分かった。32と出るはずの所に53と出て、空白ありと出る。中身を見なくても、壊れていることだけは確実に言える。
点検の結果は、サーバーの小さな保管庫に毎回上書きで置く。私が気になった時に、1行の命令で読み出せる。異常を自分から知らせてくる仕組みではないので、ここはまだ半分だと思っている。
倒し方を変えるのは、迷った。払った人を締め出す危険が、今度はこちら側に移るからだ。それでも無料側に倒したのは、2つの失敗の見え方が違うからだ。払った人が締め出されれば、その人は必ず気づいて連絡をくれる。払っていない人に有料が渡っても、誰も気づかない。気づける方の失敗を選んだ。
有料に見えていた利用者
5日間、有料プランとして使えていた利用者が実際にいた。その人には落ち度が何もない。こちらの不具合で一度渡したものを取り上げるのは、筋が悪いと考えた。
そこで、その利用者には、課金管理の画面から有料プランを期限なしで付けた。不具合の後始末としては、取り上げるより、正式に渡し直す方を選んだ。
残ったこと
「失敗したら甘い方に倒す」は、悪い判断ではない。ただ、その判断は「失敗はまれ」という前提とセットでしか成り立たない。前提が崩れた時に気づくには、失敗の回数を数えていなければならなかった。
もう1つ。開発者のアカウントで動作を確かめても、一般の利用者が通る道を確かめたことにはならない。自分だけが通れる近道を作った時点で、その近道の外側は、自分の目には見えなくなる。
コメント (0)
まだコメントはありません。最初の一言を残しませんか?