結論から
AIツールに「入力チェックを入れて」「金額を確認して」と頼むと、AIは画面側(ブラウザ)にチェックを書きます。画面側のチェックは、送る側が自由に書き換えられるので、守りにはなりません。依頼に「サーバー側で検証して」の一言を足すだけで、置き場所が変わります。
この記事で伝えたいのは、この一点だけです。
- 画面(ブラウザ)は「お願いを組み立てる場所」。金額・数量・権限・入力内容を最終的に決めるのはサーバー側
- AIは頼まれたとおり「チェック」を作るが、置き場所を指定しないと、いちばん早く動く画面側に書く
- URLやフォームの値は、誰でも書き換えられる。デモアプリでは
price=12800をprice=1に変えるだけで「¥1で確定」になった - 「サーバー側で検証して」と一言添えると、AIはサーバー側で再計算・再確認するコードを書く。頼み方の例と確かめ方を下に書いた
動画版(約2分半)は本文の上に埋め込んであります。
① 何が起きるのか:URLの金額を変えたら、¥1で確定できた
前回までと同じ、AIで作った美容室の予約アプリ(デモ用に用意した架空のもの)です。お会計ページを開くと、URLはこうなっています。
.../checkout?menu=カット+カラー&price=12800
- アドレスバーの
12800を1に書き換えて Enter - 画面は「お支払い金額 ¥1」。状態欄には「画面の値をそのまま採用(未検証)」
- 「この金額で確定する」を押せば、¥1 で予約が確定する
このアプリには「金額のチェック」が入っています。画面のフォームでマイナスや空欄を入れると、ちゃんとエラーが出ます。でもそのチェックは画面の中にしか無く、URLを直接打った人には通用しません。
② なぜ起きるのか:画面は「お願い」を組み立てる場所
理由1:ブラウザで動くものは、すべて利用者の手の中にある。
画面側のチェック(JavaScript)は、利用者のブラウザで動きます。利用者は F12 で書き換えることも、そもそも画面を使わず URL や API を直接叩くこともできます。「画面で弾かれる」は、「善意の人が間違えないようにする」ための仕組みであって、「悪意の人を止める」仕組みではありません。
理由2:サーバーは、送られてきた値をそのまま信じている。
サーバー側で「この予約の金額は、メニューの定価から自分で計算する」と決めていれば、画面から何が送られてきても関係ありません。逆に「画面が送ってきた price をそのまま保存する」と書かれていれば、¥1 も通ります。前回までの「URLの数字を変える」「/admin を開く」「RLS」も、全部この同じ話です。決めるのはサーバー側。画面は信じない。
なぜAIは画面側に書くのか。
「金額のチェックを入れて」という依頼には、置き場所が書かれていません。AIは、いちばん早く動いて、いちばん目に見える場所に書きます。画面側です。しかも動作確認では正しく弾かれるので、「入れました、動きます」と報告されます。どこに書いたかは、こちらが指定しない限り選ばれません。
これも珍しい話ではありません。2025年に公開された CVE-2025-56426 は、オープンソースのECシステム Bagisto(v2.3.6)で、カートの数量をマイナスにして送ると合計金額がゼロ以下になり、支払いなしで注文が通るというものでした。サーバーが画面から送られた値を検証せずに計算していたことが原因です。AIが書いたコードではありませんが、AIは同じ形をよく書きます。
③ どう直すのか:依頼に一言、足す
置き場所を指定します。ビフォー/アフターで比べてください。
ビフォー(画面側に書かれる)
「金額のチェックを入れて」
アフター(サーバー側に書かれる)
「金額は、画面から送られた値を使わず、サーバー側でメニューの定価から再計算して保存してください。数量・クーポン・権限(管理者かどうか)・入力の形式も、画面のチェックとは別にサーバー側で検証して、不正な値は 400 か 403 で拒否してください。画面側のチェックは残して構いませんが、それだけで済ませないでください。」
一言のポイントは3つです。「画面から送られた値を使わない」と言う(再計算させる)。「サーバー側で」と場所を言う。「画面側だけで済ませない」と言う(AIが片方だけ直すのを防ぐ)。
同じ一言が効く場面は、金額だけではありません。
- 数量・在庫:「数量はサーバー側で在庫と照合して」
- クーポン・割引:「割引率はサーバー側のクーポン表から引いて」
- 権限:「管理者かどうかはサーバー側でユーザーのフラグを見て」(→ /admin の記事)
- 所有者:「そのデータがログイン中の本人のものかをサーバー側で確認して」(→ URLの数字の記事)
- 入力の形式:「メール形式・文字数・必須項目はサーバー側でも検証して」
④ 直ったかを確かめる:画面を使わずに、送る
「画面でエラーが出た」は合格ではありません。画面を通さずに送って、サーバーが拒否するかを見ます。
- URL に金額や数量が入っているなら、直接書き換えて開く。
price=1で「¥12,800」のまま、または拒否されたら合格 - フォームなら、F12 → Elements で
<input type="hidden" name="price" value="12800">のような値を書き換えて送信。同じく定価のままなら合格 - 分からなければ、AIに「画面を通さずに price=1 を送るテストを書いて、サーバーが定価で保存することを確認して」と頼む
デモアプリの「直した版」では、price=1 を送っても「送られた ¥1 は無視 → 定価で再計算」と表示され、¥12,800 のままでした。これが合格の形です。
自分で直せないとき:エンジニアに送る一文
「お会計の金額が、URL やフォームの値をそのまま使っています(price=1 で ¥1 になります)。金額・数量・権限・入力内容を、サーバー側で再計算・再検証するようにしてください。画面側のチェックだけになっている箇所を全部洗い出してください。」
よくある質問
画面側のチェックは、消したほうがいいですか。
残してください。画面側は「利用者が間違えにくくする」ための仕組みで、役に立ちます。ただし、それは守りではありません。両方に入れて、決めるのはサーバー側、という形にします。
Stripe を使っています。金額は Stripe が守ってくれますか。
Stripe に「いくら請求するか」を伝えるのはあなたのサーバーです。その金額を画面から受け取ってそのまま Stripe に渡していれば、同じ問題が起きます。「Checkout Session や PaymentIntent の金額は、サーバー側で商品の定価から計算して」と頼んでください。
Lovable / Bolt / v0 で作りました。サーバー側とは、どこですか。
多くの場合、Supabase の Edge Functions か、データベース側の関数・RLS ポリシー・トリガーが「サーバー側」です。AIに「この検証はブラウザで動いていますか、サーバーで動いていますか」と聞くと、場所を答えてくれます。ブラウザなら、③の一言で頼み直してください。
この一言を毎回付けないといけませんか。
はい。AIは前回の依頼を「方針」としては覚えません。プロジェクトの説明ファイル(Cursor のルール、Lovable の Knowledge など)に「金額・権限・所有者・入力の検証は必ずサーバー側でも行う」と書いておくと、毎回言わずに済みます。


