ひなた英語クラブ 様
安全点検 報告書会員・予約アプリ
点検 2026年10月5日〜9日 / 報告 2026年10月9日 / naisei.dev
報告naisei
一まとめ
ひとことで言うと
会員の情報が、ログインなしで誰でも読める状態でした。点検中の10月6日にお知らせし、その日のうちに塞がっています。
- 緊急2件直った
- 高3件今週中に
- 中2件今月中に
- 低2件来月までに
今週やること
- 会員が自分を「管理者」にできる設定を直すNo.3 ・ 40分
- 申込者の名前を、管理画面で文字として表示するNo.5 ・ 30分
- 月謝の金額を、サーバー側で決めるNo.4 ・ 2時間
二点検した範囲
見たもの
- ソースコード(GitHub・閲覧のみ)
- データベースの構造と権限(点検用SQLの結果)
- 設定画面(Supabase・Vercel・Stripe)
- 使っている部品の一覧
見ていないもの
- 本番への攻撃的な試験(侵入の試み・負荷)
- 会員データの中身
- 職員のパソコン・メール
- 他社のサービスの内部
AIで候補を洗い出し、1件ずつ人が確かめました。再現は本番と切り離した開発環境で行い、本番に送ったのは、件数だけを確かめる通信1回です(10月6日 14時03分12秒・送信元 203.0.113.24)。
この点検は、見つかった問題の報告です。ほかに問題が無いことの保証ではありません。
三見つかったこと(全9件)
四1件ずつの中身
会員の情報を、ログインなしで誰でも読める・書き換えられる
- 何が起きるか
- アプリの画面に入っている「公開キー」(見えてもよい鍵)だけで、会員の表の全件を、誰でも読めて、書き換え・削除もできる状態でした。表に「行ごとの鍵(RLS)」がかかっていなかったためです。表には、会員の氏名・保護者の電話番号・メールアドレスが入っています。
- 確かめた方法
- 点検用SQLの結果で、表
membersの RLS が無効なことを確認。お客様の同意を得て、ログインなしで件数だけを確かめる通信を1回送り、312件が読める状態だと確認しました(中身は取得していません)。 - 直し方
- 表に鍵をかけ、職員だけが読み書きできるようにします。
-- 表に鍵をかける alter table public.members enable row level security; -- 職員だけが読み書きできるようにする create policy "職員だけ" on public.members for all to authenticated using (public.is_staff()) with check (public.is_staff());is_staff()は、ログインした人が職員の表に載っているかを返す関数です(あわせてお渡ししました)。 - 直したかの確かめ方
- 同じ件数の確認で「0件」になること。点検用SQLの1番が0行になること。
10分10月7日に、塞がったことを確認
あわせておすすめ:4月の運用開始から10月6日まで、この状態でした。残っているアクセスの記録(プランにより数日分)で、読まれた形跡がないかを確かめてください。個人情報が漏れたおそれがある場合、個人情報保護委員会への報告と本人への通知が必要になることがあります。必要かどうかは、委員会の窓口や弁護士にご相談ください。
問い合わせの送信口から、教室の名前で誰にでもメールを送れる
- 何が起きるか
- 問い合わせフォームの送り先が、宛先・件名・本文をブラウザから受け取っていました。この口を直接呼べば、教室のアドレス([email protected])から、誰にでも好きな内容のメールを送れます。なりすましや迷惑メールに使われると、メール配信サービスに止められ、会員への連絡も届かなくなります。
- 確かめた方法
app/api/contact/route.tsの18〜31行目で、宛先をリクエストの値から使っていることを確認。本番と切り離した開発環境(テスト用の送信設定)で、好きな宛先に送れることを再現しました。- 直し方
- 宛先は教室のアドレスに固定。自動返信は「受け付けました」の定型文だけにして、入力された本文を入れない。1つの回線から送れる回数に上限を付け、ボット対策(Cloudflare Turnstile)を入れる。
- 直したかの確かめ方
- 開発環境で宛先を指定して送っても、教室のアドレスにしか届かないこと。
30分(ボット対策は+1時間)10月7日に、直ったことを確認
会員が自分を「管理者」に書き換えられる
- 何が起きるか
- 「自分のプロフィールは自分で更新できる」設定で、名前だけでなく権限の欄(role)まで書き換えられます。会員がアプリの通信を少し書き換えるだけで自分を管理者にでき、全会員の情報と管理画面を使えるようになります。会員登録は教室の招待制のため「高」としました(誰でも登録できる設定なら「緊急」です)。
- 確かめた方法
- 点検用SQLの6番で
profiles.roleが「ログインした人が書き換えられる」と出たため、該当の設定を確認。本番と切り離した開発用のデータベースで、会員のアカウントから自分の権限を管理者に変えられることを再現しました(本番のデータには触れていません)。 - 直し方
- 会員が自分で変えられるのは名前と画像だけにし、新しい会員の行は、登録の仕組み(トリガー)で権限を「会員」に固定して作ります。職員が権限を変える操作は、中で職員かどうかを確かめる関数に移します(管理画面の操作が止まらないように)。
-- 会員が自分で変えられるのは、名前と画像だけ revoke insert, update on public.profiles from authenticated; grant update (display_name, avatar_url) on public.profiles to authenticated; - 直したかの確かめ方
- 開発環境で、会員のアカウントから権限を変えようとすると断られること。
40分これから
月謝を50円で「支払い済み」にできる
- 何が起きるか
- 月謝の支払い画面が、金額をブラウザから送っていました。サーバーはその金額のまま決済を作るので、通信を書き換えれば、たとえば50円で支払いを済ませられます。決済完了の通知でも、金額を確かめていません。
- 確かめた方法
app/api/checkout/route.tsの22行目を確認。Stripe のテスト環境で、50円の決済が「支払い済み」になることを再現しました。- 直し方
- 金額は、会員のコースからサーバーで決めます。決済完了の通知では、金額がその月の月謝と一致し、状態が「支払い済み」であることを確かめてから記録します。
// 前:金額をブラウザから受け取っていた unit_amount: body.amount, // 後:会員のコースから、サーバーで価格を決める price: PRICE_ID[member.course], - 直したかの確かめ方
- テスト環境で、金額を書き換えた決済が作られないこと。
2時間これから
申込フォームに仕込んだ命令が、管理画面で動く
- 何が起きるか
- 体験申込の一覧(管理画面)が、申込者の名前をそのまま画面に組み込んでいます。名前の欄に命令を仕込んで申し込まれると、職員が一覧を開いたときにその命令が動き、職員のログイン状態を悪用されるおそれがあります。
- 確かめた方法
components/TrialList.tsxの41行目を確認。本番と切り離した開発環境で、名前に入れた命令が、一覧を開いた時点で動くことを再現しました。- 直し方
- 名前を文字として表示します。同じ書き方がほかに2か所あるので、あわせて直します。
// 前 <td dangerouslySetInnerHTML={{ __html: trial.name }} /> // 後(文字として表示される) <td>{trial.name}</td> - 直したかの確かめ方
- 開発環境で、命令入りの名前が文字のまま表示されること。
30分これから
ここから下は、急ぎではないものです。
開発中の画面を誰でも開けて、本番のデータにつながっている
- 何が起きるか
- 開発中の画面(プレビュー)のURLに保護がかかっておらず、本番のデータベースにつながっています。URLが漏れると、作りかけで確認の甘い画面から本番のデータを触られます。
- 直し方
- Vercel のプレビューの保護(Deployment Protection)をオンにし、プレビューは開発用のデータベースにつなぎます。
30分これから
使っている部品に、公表済みの弱点が6件ある
- 何が起きるか
- 部品(npm)に、公表済みの弱点が6件(うち重要度「高」が2件)ありました。このアプリの使い方では、外から直接突ける経路は見つかりませんでしたが、使い方を変えたときに穴になります。
- 直し方
- 部品を更新して、画面の動きを確かめます。
1時間これから
管理者アカウントの一部に、2段階認証が無い
- 何が起きるか
- パスワードが漏れたとき、それだけで入られてしまいます。GitHub は設定済みで、Vercel と Supabase の管理者2名が未設定でした。
- 直し方
- それぞれのサービスの設定で、2段階認証をオンにします。
1人10分これから
管理画面を、ほかのサイトに埋め込める
- 何が起きるか
- 管理画面を見えないように別のサイトへ埋め込み、職員に気づかれないままボタンを押させる手口に使えます。
- 直し方
- 埋め込みを断る設定(HTTPヘッダーの
frame-ancestors 'none')を、next.configの headers に1行足します。
10分これから
五できていたこと
- データベースの強い鍵と決済の秘密鍵は、サーバー側だけに置かれていました。画面のコードにも、変更の履歴にも入っていません。
- 決済完了の通知は、Stripe の署名を確かめていました(No.4 の金額の照合を足せば十分です)。
- データベースの毎日のバックアップが有効でした。
- Next.js は、2025年12月に公表された重大な弱点の修正版になっていました。
六直す順番
今週
No.3 → No.5 → No.4あわせて約3時間
今月
No.6・No.7あわせて約1時間半
来月まで
No.8・No.9あわせて約30分
直し終わったら、このメールに「直しました」とだけ返信してください。直ったかどうかを、こちらでもう一度確かめます(1回分は料金に含まれています)。
七点検した項目
問題なし見つかった対象外
- 秘密の鍵の置き場所問題なし
- 変更の履歴に残った鍵問題なし
- 公開してはいけないファイル問題なし
- 外から呼べる口(10か所)の確認No.2・4
- メール・LINEを送る口No.2
- お金の計算No.4
- 権限の決め方No.3
- IDの付け替えで他人のデータ問題なし
- 入力の画面への表示No.5
- データベースの鍵(RLS)No.1・3
- ビューと関数の権限問題なし
- ファイルの保存場所問題なし
- ログインの設定問題なし
- 開発中の画面No.6
- 部品の古さNo.7
- 管理者アカウントの2段階認証No.8
- 埋め込み・ヘッダーの設定No.9
- AI開発ツールの権限問題なし
- Firebase・Google Apps Script対象外