フューチャー技術ブログ

Vue.jsで脆弱なアプリを作って学ぶ、セキュリティ面で気をつけたいポイント

image.png

※上記サムネイルはChatGPTにて本記事を読み込ませて生成しました。

Future Value Group(FVG)の永井優斗です。

Vue.js連載の8本目です。Vue.js は使いやすく、直感的なテンプレート構文でフロントエンド開発を加速してくれます。しかし、その便利さゆえに「セキュリティをうっかり見落とす」ことがあるのも事実です。

この記事では、Vue.jsで実際にわざと脆弱なアプリを作り、そこからXSS・Cookie・CSRF・レート制限といった脆弱性を一気に体験してみます。ちなみにソースの8割ぐらいは生成AIに生成してもらいました。この記事も生成AIと相談しながら書きました。すごいね。

教材アプリはこちらのGitHub上で公開しています。

※試すことを優先しているのでUIはシンプルに。そのうちきれいにしたいなあ…

はじめに

今回の教材アプリは Vue 3 + Vite で構築し、バックエンドには Express を利用します。

「脆弱版」と「安全版」を切り替えて、落とし穴と対策を比較できる構成です。

01-vulnerable/   # ← わざと脆弱な実装
02-safe/ # ← 対策を施した実装

ローカルで起動してお試しください。

教材アプリはセキュリティの知識を駆使して情報を抜き取るゲームである、「CTF(Capture The Flag)」のような形で作成しており、脆弱版では各課題にて「攻撃」が成功すると100点のスコアがGetできます。全部で3問あります。

この記事はCTFの「答え」にあたるものでもあるので、セキュリティ知識を試したい方は先に教材アプリを試してみてから本記事をお読みください。

警告
意図的に脆弱性をふくめているアプリケーションですので、公開環境やインターネットに接続された環境で実行しないでください。学習用にローカルで閉じた環境でのみ利用してください。

また、本記事や教材アプリは代表的な脆弱性の学習のための資料であり、攻撃を目的としたものではございません。

起動方法:

cd 01-vulnerable
npm install
npm run server # APIサーバ (http://localhost:3001)
npm run dev # フロントエンド (http://localhost:5173) 上のnpm run serverとは別のターミナルで実行してください

1. DOM XSS(v-htmlの乱用)

Vue.js では、テンプレートの中で変数を埋め込む際、{{ userInput }} のように記述すると、自動的にHTMLがエスケープされます。
そのため、通常の使い方ではユーザーの入力からXSSは発生しません。

しかし、「HTMLを直接描画したい」という欲求に負けて v-html を使うと、XSSの標的になるかもしれません。

v-htmlとは

<div v-html="someHtml"></div>

このように書くと、someHtml の中身がそのままHTMLとして挿入されます。

つまり、ユーザー入力が <p>こんにちは</p> なら、実際に <p> タグが生成されます。そしてユーザー入力が <img src=x onerror="alert(1)"> だったら……?そのままスクリプトが実行されます。

実際にXSSを起こしてみよう

教材アプリでは「脆弱UI(XSS)」という課題ページがあります。

手順:

  • テキストエリアに以下を入力してください。
<img src=x onerror="alert(window.__FLAG_XSS)">
  • 下のプレビュー領域が v-html で描画されています。

そこに画像タグが挿入され、onerror 属性が評価されるとアラートが表示されます。ここで表示される window.__FLAG_XSS は教材用のフラグでグローバルな秘密変数を想定しています。実運用ならこのような「グローバルな秘密変数」は致命的です。

攻撃を試してみて何も起きないときは以下を確認してみてください。

  • <script> タグは innerHTML 経由では実行されません。イベント属性(onerror / onload)を使ってください
  • CSP(Content-Security-Policy)が有効な場合、inline スクリプトがブロックされることがあります
  • 要素が正しく挿入されているか、開発者ツールの Elements タブで確認しましょう

対策

  • v-html を安易に使わない
  • どうしてもHTMLを扱う場合は、ホワイトリスト方式のサニタイズを適用する
  • ユーザー入力は常に 「テキスト」として表示 することを原則に

2. Cookieの危険な使い方

続いて、Cookie周りの落とし穴です。

教材の2つ目の課題は「CookieにセッションIDを保存しているが、HttpOnly が設定されていない」例を取り扱います。

document.cookieとは

ブラウザ内でアクセス可能なCookieを読み取るAPIです。

console.log(document.cookie)
// => "sid=demo-session; theme=light"

しかし本来、セッションIDなどの機密情報はサーバー専用で使うべきです。
それをHttpOnly無しで発行すると、XSSを使って誰でも読める状態になります。

Cookieからsidを盗みだす

脆弱版では sid というCookieが HttpOnly: false で発行されています。
そのため、以下のコードで誰でも読み取れます。

<img src=x onerror="alert(document.cookie)">

もしアラートに sid=demo-session と出たら、XSSがCookieを盗み出せたということです。
攻撃者はこの値を使って、別の環境で「なりすましログイン」することができます。

攻撃を試してみてアラートが空だった場合は以下を確認してみてください

  • HttpOnly が有効になっている(安全版を開いていないか?)
  • オリジンが異なる(5173と3001問題)
  • Viteのプロキシ設定でcookieDomainRewriteが漏れている

対策

  • セッションIDなどのCookieには必ずHttpOnlyとSecureを付与
    • SameSite=Lax 以上を設定してクロスサイト送信を制限する
    • 機密情報をフロント側のJSから参照できないように設計する

3. CSRF

次に、CSRF(クロスサイトリクエストフォージェリ)の課題です。

バックエンドの話も入ってしまうんですが、フロントエンドの開発者も知っておくべきだと思うので入れてみました。

ここでは、ユーザーが意図していない送金が実行されてしまう脆弱なAPIを見ます。

CSRFとは?

ユーザーが認証済み状態(Cookieを持っている)で、攻撃者のサイトを開いたとき、そのブラウザが自動的にCookieを添えてリクエストを送ってしまう、これがCSRF攻撃の基本構造です。

image.png

情報処理推進機構(IPA):安全なウェブサイトの作り方 - 1.6 CSRF(クロスサイト・リクエスト・フォージェリ)より引用

意図せず残高を減らす「罠ページ」

教材の「CSRF」課題では、次のようなAPIがあります。

GET /api/csrf/transfer?to=attacker&amount=100
  1. /api/wallet を開くと、現在の残高が表示されます(例:1000)
  2. 攻撃者ページを開きます(以下を保存してブラウザで開く):
<html>
<body>
<h1>かわいいねこの画像だよ!</h1>
<img src="http://localhost:5173/api/csrf/transfer?to=attacker&amount=100">
</body>
</html>
  1. ページを開くと自動的にリクエストが発生します
  2. /api/walletを再確認すると、残高が減っていることを確認します

※ 教材アプリでは攻撃が成功した際にスコア付与のため、csrfCode が付与されています。それをフォームに入力して提出してください。

対策

  1. 副作用のある操作はGETにしない(POST限定)
  2. CSRFトークンを導入する
// 発行時
res.cookie('csrfToken', token, { httpOnly: false, sameSite: 'lax' })

// 検証時
if (req.cookies.csrfToken !== req.headers['x-csrf-token']) {
return res.status(403).json({ error: 'csrf check failed' })
}
  1. SameSiteを適切に設定(Lax or Strict)
  2. Referer / Origin チェックを補助的にする
  3. レート制限で濫用防止(次の章で説明します)

4.(補足) レート制限なし

CSRFやXSSなどの被害を拡大させるのは「無限リクエスト」です。

同一IPやユーザーが短時間に何百回もアクセスできる状態は危険です。

Expressで簡単にレート制限を導入する

以下のようにexpress-rate-limitを設定することで簡単にレート制限を導入できます。

index.js
import rateLimit from 'express-rate-limit'

const limiter = rateLimit({
windowMs: 30_000, // 30秒
max: 10,
message: { ok:false, error: 'rate limited' }
})

app.post('/api/transfer', limiter, (req, res) => {
// 安全な送金処理
})

これだけで「30秒に10回まで」という制限を導入できます。

短いコードですが、セキュリティ上の効果は絶大です。

まとめ

脆弱性 原因 対策
DOM XSS v-htmlの乱用 サニタイズ、{{ }}で表示
Cookie漏洩 HttpOnlyなし HttpOnly, Secure, SameSite設定
CSRF GETで副作用、トークン未検証 POST限定、CSRFトークン
レート制限なし 無限アクセス express-rate-limit などで制限

最後に

Vue.js は便利ですが、安全性を担保する仕組みは「フレームワークまかせ」では不十分です。

「なぜ危ないのか」を一度体験しておくことで、脆弱性への実感を持ってもらえたらうれしいです!

脆弱なアプリを作らないように気をつけながらフロントエンド開発をEnjoyしましょう!

GitHubリポジトリ
vue-ctfs(脆弱版と安全版を含む)