🍳

[ハンズオン]Webアプリの代表的な脆弱性を体験して、安全なコードの書き方を学ぼう

に公開

1. はじめに

Webアプリケーション開発において、セキュリティは避けて通れない重要なトピックです。しかし、座学だけで「XSS(クロスサイト・スクリプティング)」や「SQLインジェクション」を理解するのはなかなか難しいと思います。

この記事では、あえて脆弱性を含む「脆弱なECサイト」を使って、代表的な脆弱性をハンズオン形式で体験してもらうためのものです。
実際に体験することで、その仕組みや危険性を理解し、基本的な対策を学ぶことを目的にしています。


2. ハンズオンの準備

初めに、自分のパソコンにハンズオン環境を構築します。

2-0 Docker Desktop インストール

Docker Desktop
https://docs.docker.com/get-started/introduction/get-docker-desktop/

2-1. リポジトリのクローン

ターミナルを開き、以下のリポジトリをクローンします。

git clone https://github.com/kykt35/vulnerable-shop.git
cd vulnerable-shop

2-2. Dockerで起動

docker compose コマンドを使って、ECサイトと攻撃者用サイトの2つを起動します。

docker compose up --build

起動が完了すると、以下のURLでアクセスできるようになります。

  • ショップサイト(脆弱): http://localhost:8000
  • 攻撃者用サイト: http://localhost:9000

スクリーンショット: 起動したショップサイトのトップ画面
*▲ 起動したショップサイト。


3. ハンズオン

3-1. XSS (Cross-Site Scripting)

概要: ユーザーの入力値がそのまま画面に出力される不備を突き、悪意のあるスクリプトを実行させる攻撃です。

🚨 体験:検索窓からスクリプトを注入

ショップサイトの「検索窓」に、以下の文字列を入力してみましょう。

<script>alert('XSS攻撃成功!');</script>

スクリーンショット: 検索窓へスクリプトを入力

スクリーンショット: XSS攻撃によりアラートが表示される様子
▲ 検索ボタンを押した瞬間、ブラウザでJavaScriptが実行されました。

🔍 なぜ起きたのか?(XSS)

原因は、View(テンプレート)側で入力値をエスケープせずに出力してしまったことです。
たとえばEJSでは、以下のように 非エスケープ出力(<%- %>)を使うと、コメントに含まれるHTML/JavaScriptがそのままHTMLとして解釈されてしまいます。

  <h4 style="margin: 12px 0 6px;">投稿一覧</h4>
  <% comments.forEach((c) => { %>
    <div class="card stack">
      <div class="meta"><%= c.created_at %> / <strong><%= c.username %></strong></div>
      <div>
        <!-- XSS脆弱性: 無エスケープ -->
        <%- c.body %>
      </div>
    </div>
  <% }) %>

このとき、コメントに <script>alert('XSS攻撃成功!');</script> が入っていると、サーバーが返すHTMLは概ね次のようになります(=スクリプトがHTMLとして混入します)。

<!-- ブラウザが受け取るHTML(イメージ) -->
   <h4 style="margin: 12px 0 6px;">投稿一覧</h4>
   <div class="card stack">
      <div class="meta">2026-01-30T05:50:00.543Z / <strong>alice</strong></div>
      <div>
        <!-- XSS脆弱性: 無エスケープ -->
        <script>alert('XSS攻撃成功!');</script>
      </div>
   </div>

その結果、ブラウザは受け取った文字列を「HTML/JavaScript(命令)」として解釈し、スクリプトを実行してしまいます。

🚩 危険性(XSS)

XSSが本当に危険なのは、単なる alert() 表示で終わらず、そのサイト上で「ユーザーになりすまして操作」できてしまう点です。たとえば以下のような被害につながる可能性があります。

  • セッション乗っ取り: Cookieやトークンを盗まれ、ログイン状態を奪われる(設定次第では盗めない場合もありますが、被害はこれに限りません)
  • 個人情報の窃取: 画面に表示される氏名・住所・注文履歴などを外部に送信される
  • 不正操作: 「購入」「退会」「メールアドレス変更」などを勝手に実行される
  • フィッシング誘導: 正規サイト上に偽ログインフォームを表示し、認証情報を入力させる

✅ 対策:エスケープ処理の徹底

特別な意味を持つ文字(<, >, &, ", ')を、無害な文字列(&lt;, &gt; など)に変換するHTMLエスケープを行います。
最近のモダンなフレームワーク(React, Vue, またEJSの <%= %> など)は標準でエスケープしてくれます。ただし、あえてエスケープしない関数(EJSの <%- %> など)も用意されています。こちらは安易に使わないことを強く推奨します。


3-2. SQL Injection

概要: データベース操作命令(SQL)を不正に注入し、データの盗聴や改ざんを行う攻撃です。

🚨 体験:ログインをバイパスする

ログイン画面で、ユーザー名に以下を入力します(パスワードは何でもOK)。

'_OR_1=1_--_    *アンダースペース(_)を半角スペースに置き換えてください.

スクリーンショット: SQLインジェクションでログインに成功した画面
▲ パスワードを知らなくても、DBの最初のユーザー(aliceなど)としてログインできてしまいました。

🔍 なぜ起きたのか?(SQL Injection)

サーバー側で、SQL文を文字列結合で組み立てていることが原因です。

// server.js(脆弱な例)
const sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";

入力した '_OR_1=1_--_ が組み合わさることで、SQLの意味が「ユーザー名が一致、または 1=1(常に真)」に書き換わり、認証が意味をなさなくなりました。

🚩 危険性(SQL Injection)

SQLiは、ログイン突破だけでなく DBそのものを好きに読んだり壊したり され得ます。

  • 認証回避: パスワード認証を回避してログイン
  • 情報漏えい: ユーザー情報、パスワードハッシュ、購入履歴、APIキー等の大量流出
  • 権限昇格: 管理者フラグを書き換えられ、管理画面を乗っ取られる
  • 改ざん/破壊: 注文金額の改ざん、在庫の操作、最悪はテーブル削除(DROP TABLE)など

✅ 対策:プレースホルダ(静的プリペアドステートメント)

SQLの「構造」と「データ」を分離します。

// 安全な例
db.prepare("SELECT * FROM users WHERE username = ? AND password = ?").get(username, password);

このようにプレースホルダ(?)を使うことで、入力値は必ず「単なるデータ」として処理され、命令として実行されることはありません。


3-3. フォーム値改ざん (Parameter Tampering)

概要: ブラウザ側の HTML(hidden 項目など)を書き換え、開発者が想定しない値を送信する攻撃です。

🚨 体験:1,200円の商品を1円で購入する

商品購入画面で、右クリック →「検証」で、デベロッパーツールを開きます。
「注文確定」ボタン付近にある hidden 項目を探し、価格を 1 に書き換えてみましょう。

<!-- 修正前 -->
<input type="hidden" name="unit_price_yen" value="1200">
<!-- 修正後 -->
<input type="hidden" name="unit_price_yen" value="1">

スクリーンショット: デベロッパーツールでhidden属性の値を書き換える様子

数量を10に変更すると、合計金額10円になります。このまま注文を確定すると、注文履歴に「10円」で記録されてしまいます。

🔍 なぜ起きたのか?(パラメータ改ざん)

サーバー側が、ブラウザから送られてきた「単価」をそのまま信じて保存してしまったからです。

🚩 危険性(パラメータ改ざん)

フロント側(ブラウザ)から送られてくるパラメータは、攻撃者によって自由に改ざん可能です。

  • 金銭被害: 価格・送料・ポイント・割引額などを改ざんされ、不正購入される
  • 権限/IDのすり替え: user_id や is_admin などを改ざんし、他人のデータ閲覧・操作につながる(IDORの入口になりやすい)
  • 業務ロジック破壊: 在庫数、申請ステータス、利用上限などの制約を回避される

✅ 対策:重要なロジックはサーバー側で処理する

「クライアントから送られてくる値は、全て攻撃者によって書き換えられている」と想定すべきです。
価格などの重要なデータはフロントから送らせるのではなく、サーバー側のDBから再取得して計算するようにしましょう。


3-4. CSRF (Cross-Site Request Forgery)

概要: 罠サイトを踏ませることで、ログイン中のユーザーに意図しない操作を強制させる攻撃です。

🚨 体験:罠サイトをクリックしたら勝手にコメントが投稿された

  1. ショップサイトにログインしたままにします。
  2. 別タブで「攻撃者用サイト(http://localhost:9000/csrf)」を開きます。
  3. 「🎁 景品を受け取る」ボタンをクリックします。

スクリーンショット: 攻撃者用サイトの「景品を受け取る」ボタン

ショップサイトの商品詳細(Coffee Beans)を確認すると、自分が操作していないのにコメントが投稿されています。
スクリーンショット: 商品ページにコメント

🔍 なぜ起きたのか?(CSRF)

ブラウザは、特定のサイトにリクエストを送る際、そのサイトの Cookie(ログインセッション情報)を自動的に付与して送信する性質があります。攻撃用サイトはこの性質を悪用し、ユーザーのブラウザに「ショップサイトへのコメント投稿リクエスト」を強制的に投げさせたのです。

🚩 危険性(CSRF)

CSRFは「ユーザー本人が操作したように見える」のが厄介です。被害はコメント投稿に限らず、状態を変更する操作(POST/PUT/DELETE等) 全般で起こりえます。

  • 意図しない操作の強制: メールアドレス変更、パスワード変更、退会、配送先変更など
  • 金銭的被害: 商品購入、サブスク契約、送金(決済導線の作り次第で成立)
  • 連鎖攻撃: 変更後のメールアドレスを攻撃者のものにされ、以降のアカウント復旧まで奪われる

✅ 対策:CSRFトークンとSameSite属性

  • CSRFトークン: 画面ごとに発行されるトークンをリクエストに含め、サーバー側で照合する。
  • Cookie の SameSite 属性: SameSite=Lax や Strict を設定することで、外部サイトからの遷移時に Cookie を送信しないよう制限できます。

4. 💡 セキュリティ対策の3つの鉄則

ハンズオンを通じて見えてきた、安全なWebアプリ開発の基本です。

  1. 「全ての入力」を疑う
    • フォーム、URLパラメータ、Cookie、hidden項目。フロントから来るデータは全て改ざん可能だと考えましょう。
  2. 「文脈」に合わせて適切に出力する
    • HTMLならHTMLエスケープ、SQLならプレースホルダ。データが命令として解釈されないよう境界を明確にします。
  3. 重要なロジックは「サーバー側」で完結させる
    • フロント側のチェックはUX(ユーザー体験)のため。セキュリティの担保は必ずサーバー側で行います。

5. 📚 さらに学びたい方へ

  • IPA「安全なウェブサイトの作り方」: Web脆弱性対策のバイブルです。
  • 体系的に学ぶ 安全なWebアプリケーションの作り方(徳丸本): 非常に詳しく、実習環境も充実しています。

セキュリティを学んで安全で楽しい開発ライフを!

Discussion