👀

AIにECサイトを作らせてセキュリティスキャンにかけたら「動くけど守れない」コードだった

に公開
3

はじめに

こんにちは。株式会社エーアイセキュリティラボで、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スキャンサマリー画面。深刻度ごとの件数グラフとOWASP TOP 10カテゴリーのレーダーチャートが表示されている
AeyeScanのスキャンサマリー。Critical/Highは0件だが、Medium以下で24件の指摘が出ている

CriticalもHighもゼロ。ぱっと見は悪くない結果に見えます。しかし、この数字の中身を掘っていくと、バイブコーディングの「クセ」がはっきり見えてきます。

特に注目すべき検出結果

24件すべてを列挙するのではなく、「バイブコーディングの特徴」が顕著に表れているものに絞って紹介します。

クリックジャッキング(Medium / CVSS 5.3)

会員登録フォーム(/register)が外部サイトにiframe(別のWebページの中にページを埋め込む仕組み)で埋め込める状態でした。

外部サイトにiframeでregisterページを埋め込んだ検証画面。「アカウントを作成」ボタンを含むフォームがそのまま表示されている
外部サイトにiframeで /register を埋め込んだところ、会員登録フォームがそのまま表示された(AeyeScanによる検証画面)

攻撃者が罠サイト上に透明なiframeを重ねれば、ユーザーに気づかせないまま送信ボタンをクリックさせることができます。

原因: Content-Security-Policyframe-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「安全なウェブサイトの作り方」対応状況サマリー。クリックジャッキングのみ×で、他の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文字で通ります。全部「動作には関係ないけど、セキュリティ上は問題」なものばかりです。

「動くものを作る」と「安全なものを作る」の間にはギャップがあります。そのギャップを埋めるのは、今のところまだ人間の仕事です。

参考リンク

3
株式会社エーアイセキュリティラボ 開発本部

Discussion

ログインするとコメントできます
3