AIにECサイトを作らせてセキュリティスキャンにかけたら「動くけど守れない」コードだった
はじめに
こんにちは。株式会社エーアイセキュリティラボで、SaaS型Webアプリケーション脆弱性診断プラットフォーム「AeyeScan」の開発エンジニアをしている有馬です。
バイブコーディング(AIに自然言語で指示してコードを書かせる開発スタイル)が当たり前になりつつある今、AIが生成したコードのセキュリティは実際どうなのか。推測ではなくデータで答えを出したくて、ひとつ検証をしてみました。Claude にECサイトをゼロから作らせて、完成したアプリを自社のAeyeScanでスキャンする、というものです。
先に結論だけ書きます。Critical・Highの脆弱性は0件。でも、24件の問題が見つかりました。 その内訳を見ていくと、「AIはセキュリティを知らないのではなく、セキュリティを気にしない」という構造が見えてきました。
検証の概要
作ったもの
ECサイト実装用のプロンプト自体も Sonnet 4.6 に生成させています。こちらから指定したのはNext.js・Express・shadcn/uiという大枠と、ECサイトとしての基本機能、ローカル環境で動かすことだけです。
セキュリティに関する指示は一切入れていません。
実際に使用したプロンプト(クリックで展開)
# ECサイト実装プロンプト
## 依頼内容
Next.js(フロントエンド)とExpress(バックエンド API)を使ったECサイトをフルスタックで実装してください。
ローカル環境で動作すればOKです。クラウドへのデプロイは不要です。
## 技術スタック
| レイヤー | 技術 |
|--------|------|
| フロントエンド | Next.js(App Router) |
| UIコンポーネント | shadcn/ui |
| バックエンド API | Express.js(REST API) |
| データベース | SQLite(better-sqlite3) |
| 認証 | JWT(jsonwebtoken) + bcrypt |
| ORM | なし(生SQL、better-sqlite3で直接操作) |
## ディレクトリ構成
ec-site/
├── frontend/ # Next.js アプリ
│ ├── app/
│ ├── components/
│ └── package.json
├── backend/ # Express アプリ
│ ├── routes/
│ ├── middleware/
│ ├── db/
│ └── package.json
└── README.md
## 実装する機能
### ユーザー認証
- ユーザー登録(メールアドレス・パスワード・名前)
- ログイン / ログアウト
- JWT によるセッション管理
- ログイン済みユーザーのみアクセスできるページ(マイページ、カート、注文)
### 商品機能
- 商品一覧ページ(カテゴリ絞り込み・キーワード検索・並び替え)
- 商品詳細ページ(商品名・説明・価格・在庫数・画像URL)
- 商品レビュー投稿・一覧表示
### カート機能
- カートへの商品追加・削除・数量変更
- カート内容の合計金額表示
### 注文機能
- カートから注文確定
- 注文履歴一覧ページ
- 注文詳細ページ(注文日・商品・金額・ステータス)
### 管理機能(シンプルな管理者ページ)
- 商品の追加・編集・削除
- 注文一覧の確認・ステータス変更
### ユーザーページ
- プロフィール表示・編集(名前・メールアドレス・住所)
- 注文履歴
## データベーステーブル設計
以下のテーブルをSQLiteで作成してください。
-- ユーザー
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
address TEXT,
role TEXT DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 商品カテゴリ
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);
-- 商品
CREATE TABLE products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price INTEGER NOT NULL,
stock INTEGER DEFAULT 0,
category_id INTEGER REFERENCES categories(id),
image_url TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- カート
CREATE TABLE cart_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER DEFAULT 1
);
-- 注文
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
total_price INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
shipping_address TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 注文明細
CREATE TABLE order_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER REFERENCES orders(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price INTEGER NOT NULL
);
-- レビュー
CREATE TABLE reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
rating INTEGER NOT NULL,
comment TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
## Express API エンドポイント一覧
### 認証 /api/auth
POST /api/auth/register # ユーザー登録
POST /api/auth/login # ログイン(JWT返却)
POST /api/auth/logout # ログアウト
GET /api/auth/me # ログイン中ユーザー情報取得
### 商品 /api/products
GET /api/products # 商品一覧
GET /api/products/:id # 商品詳細
POST /api/products # 商品追加(管理者のみ)
PUT /api/products/:id # 商品更新(管理者のみ)
DELETE /api/products/:id # 商品削除(管理者のみ)
GET /api/products/:id/reviews # レビュー一覧
POST /api/products/:id/reviews # レビュー投稿(ログイン必須)
### カート /api/cart
GET /api/cart # カート内容取得(ログイン必須)
POST /api/cart # 商品追加(ログイン必須)
PUT /api/cart/:itemId # 数量変更(ログイン必須)
DELETE /api/cart/:itemId # 削除(ログイン必須)
### 注文 /api/orders
GET /api/orders # 注文履歴(ログイン必須)
POST /api/orders # 注文確定(ログイン必須)
GET /api/orders/:id # 注文詳細(ログイン必須)
PUT /api/orders/:id/status # ステータス更新(管理者のみ)
### ユーザー /api/users
GET /api/users/me # プロフィール取得(ログイン必須)
PUT /api/users/me # プロフィール更新(ログイン必須)
GET /api/users # ユーザー一覧(管理者のみ)
## フロントエンド画面一覧
| パス | 画面 |
|-----|------|
| / | トップページ(おすすめ商品) |
| /products | 商品一覧 |
| /products/[id] | 商品詳細 |
| /cart | カート |
| /checkout | 注文確認・確定 |
| /orders | 注文履歴 |
| /orders/[id] | 注文詳細 |
| /mypage | マイページ |
| /login | ログイン |
| /register | 新規登録 |
| /admin | 管理者ダッシュボード |
| /admin/products | 商品管理 |
| /admin/orders | 注文管理 |
## 初期データ
アプリ起動時に以下のシードデータを投入してください。
- 管理者アカウント: admin@example.com / password: admin123 / role: admin
- 一般ユーザー: user@example.com / password: user123 / role: user
- カテゴリ: 家電、衣類、食品、書籍、スポーツ
- 商品: 各カテゴリ3〜5件程度(名前・価格・説明・在庫・画像URLはダミーで可)
## その他の注意点
- フロントエンドからバックエンドへのリクエストは http://localhost:3001 を向くようにしてください
- JWTトークンはフロントエンドで localStorage または Cookie に保存してください
- shadcn/ui のコンポーネント(Button, Input, Card, Table, Dialog など)を積極的に使ってください
- レスポンシブ対応(スマホ・PC)をしてください
- エラーハンドリング(404、認証エラー、バリデーションエラー)も実装してください
- フォームバリデーションはフロントエンド・バックエンド双方で行ってください
AIが実際に生成したアプリの構成
| レイヤー | 技術 |
|---|---|
| フロントエンド | Next.js 16 (App Router) + TypeScript + Tailwind CSS |
| UIコンポーネント | shadcn/ui + lucide-react |
| フォーム管理 | react-hook-form + zod |
| バックエンド | Express.js 4.18.3 |
| データベース | SQLite (better-sqlite3) |
| 認証 | JWT + bcrypt |
会員登録・ログイン、商品一覧・検索、カート管理、注文確定、レビュー投稿、管理者ダッシュボードなど、ECサイトとして一通りの機能を持っています。こちらから指定したのはフレームワークと基本機能だけで、それ以外のライブラリ選定や具体的な実装はすべてAIに任せました。
スキャン方法
完成したアプリをローカル開発環境で起動し、AeyeScan のWebアプリケーションスキャンを実施しました。
- 対象画面数: 40画面
- スキャンルールセット: Webアプリケーションスキャン(OWASP TOP 10 全カテゴリ対応)
スキャン結果のサマリー
全体評価: Medium
| 深刻度 | 件数 |
|---|---|
| Critical | 0件 |
| High | 0件 |
| Medium | 2件 |
| Low | 4件 |
| Info | 18件 |
AeyeScanのスキャンサマリー。Critical/Highは0件だが、Medium以下で24件の指摘が出ている
CriticalもHighもゼロ。ぱっと見は悪くない結果に見えます。しかし、この数字の中身を掘っていくと、バイブコーディングの「クセ」がはっきり見えてきます。
特に注目すべき検出結果
24件すべてを列挙するのではなく、「バイブコーディングの特徴」が顕著に表れているものに絞って紹介します。
クリックジャッキング(Medium / CVSS 5.3)
会員登録フォーム(/register)が外部サイトにiframe(別のWebページの中にページを埋め込む仕組み)で埋め込める状態でした。
外部サイトにiframeで /register を埋め込んだところ、会員登録フォームがそのまま表示された(AeyeScanによる検証画面)
攻撃者が罠サイト上に透明なiframeを重ねれば、ユーザーに気づかせないまま送信ボタンをクリックさせることができます。
原因: Content-Security-Policy の frame-ancestors ディレクティブ(ブラウザに「このページは外部サイトに埋め込まないで」と伝えるセキュリティヘッダ)が設定されていませんでした。この設定は明示的に行う必要がありますが、AIはこれを行っていませんでした。
セキュリティミドルウェアの「入れただけ」問題
今回の検証で一番印象的だった発見です。
スキャン結果にはセキュリティヘッダの不備(CSP、X-Content-Type-Options、Referrer-Policy等)が多数含まれていました。原因を探るためにソースコードを確認したところ、こうなっていました。
// server.js の実際の内容 ── helmet のインポートも適用もない
app.use(cors({ origin: true, credentials: true }));
app.use(pinoHttp({ logger }));
app.use(express.json());
// ← ここに app.use(helmet()) があるべきだった
AIはExpressのセキュリティミドルウェア「Helmet」(セキュリティ関連のHTTPヘッダをまとめて付与してくれるライブラリ)をpackage.jsonにインストールしていたものの、app.use(helmet()) を書いていなかったのです。同様に、レート制限ミドルウェアの express-rate-limit も依存関係に追加されているのに、実際のコードでは一切使われていませんでした。
「セキュリティに必要なパッケージを知っていて、インストールまではする。でも実際に適用するコードは書き忘れる」——これがバイブコーディングの典型的な落とし穴だと感じました。
パスワード強度の不備
7文字のパスワード user123 で会員登録が成功してしまいました。実装を確認すると6文字以上のバリデーションは存在していたものの、OWASP(Webセキュリティの国際的な基準を策定する団体)の基準では最低8文字以上が推奨されており、さらによくあるパスワードを拒否する仕組みも必要です。基準が緩すぎる状態でした。
デバッグ情報の露出
Express側のエラーハンドラが、スタックトレースをそのままクライアントに返していました。
// server.js のエラーハンドラ
res.status(500).json({
error: err.message,
stack: err.stack, // ← サーバーのディレクトリ構造がそのまま返される
});
攻撃者にアプリの内部構造を教えてしまう実装です。本番では NODE_ENV で出し分けるべきですが、その考慮もありませんでした。
未検出だった項目にも注目する
検出された問題と同じくらい大事なのが、検出されなかった項目です。
IPA「安全なウェブサイトの作り方」の11カテゴリ中、クリックジャッキング以外の10項目はすべて合格していました。
IPA「安全なウェブサイトの作り方」対応状況。11項目中10項目が「○」で、「×」はクリックジャッキングのみ
SQLインジェクション、XSS、CSRF、ディレクトリ・トラバーサルといった致命的な脆弱性はすべて未検出でした。実際にソースコードを確認すると、データベースへのクエリはすべてbetter-sqlite3のprepared statement(SQL文にユーザー入力を安全に埋め込む仕組み)で記述されており、ユーザー入力が直接SQLに結合される箇所はありませんでした。AIが意識的にセキュリティ対策を施したというよりも、フレームワークの標準的な書き方に従った結果、自然と防がれていたと考えられます。
結果から見えるバイブコーディングの構造的な特徴
ここまでの結果を整理すると、ひとつの構造が浮かんできます。
フレームワークが自動で守る領域(XSS、SQLインジェクション等)は問題なし。一方で、明示的に設定しなければ効かない防御(CSP、パスワードポリシー等)は軒並み抜けている。
そして検出された問題には共通点があります。どれも「なくてもアプリは動く」ものばかりです。CSPヘッダがなくてもサイトは表示されますし、パスワードが7文字でも登録は通りますし、Helmetが未適用でもExpressは起動します。
AIは「動くものを作る」ことには長けています。ただ、動作に影響しないセキュリティ設定は、こちらから頼まない限り後回しにされます。Helmetをインストールする判断力はあるのに、app.use(helmet()) の一行は書かない。「知っている」と「最後までやりきる」の間にギャップがあるのです。
バイブコーディング時代のセキュリティ対策
今回の検証から得た教訓を4つにまとめます。
1. セキュリティ要件をプロンプトに含める。 「ECサイトを作って」ではなく、「Helmetを適用して」「CSPのframe-ancestorsを設定して」「パスワードは8文字以上で」と具体的に指示します。これだけでAIの出力は大きく変わります。
2. 「インストール済み」と「適用済み」を区別する。 package.json にあるからといって使われているとは限りません。Helmetとexpress-rate-limitがまさにそうでした。セキュリティライブラリが実際にコード上で適用されているかを確認することが重要です。
3. フレームワーク任せで安心しない。 Next.jsがXSSを防いでくれるのは事実ですが、それはフレームワークの守備範囲の話です。CSPヘッダやパスワードポリシーなど、フレームワークの「外側」にあるセキュリティは明示的に実装する必要があります。
4. スキャンツールで機械的にチェックする。 40画面のアプリで24件。目視では拾いきれない数です。AIで速く作り、スキャンツールで網羅的に検査し、人間が最終判断する。この3段階のサイクルが現実的なやり方だと考えています。
まとめ
AIにECサイトを作らせてスキャンした結果、見えたのは「AIは賢いが、気が利かない」ということでした。
致命的な脆弱性はゼロ。フレームワークが守ってくれる領域は問題ありません。しかしHelmetは入れただけで適用し忘れていますし、パスワードは7文字で通ります。全部「動作には関係ないけど、セキュリティ上は問題」なものばかりです。
「動くものを作る」と「安全なものを作る」の間にはギャップがあります。そのギャップを埋めるのは、今のところまだ人間の仕事です。
Discussion