1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CSP connect-src 'none' で完結する家計簿Webアプリを作って Cloudflare Pages で運用してる話

1
Last updated at Posted at 2026-04-28

CSP connect-src 'none' で完結する家計簿Webアプリを作って Cloudflare Pages で運用してる話

みらいコンパス - CSP connect-src 'none' で完結する家計簿Webアプリ

TL;DR

  • 家計データを扱うWebアプリで「外部送信を物理的に不可能」にしたかった
  • CSP connect-src 'none' + localStorage 完結 で実装
  • バックエンド0円・サーバ0円・ユーザーアカウント0で運用中
  • WordPress (ブログ) + Cloudflare Pages (アプリ本体) の構成
  • Service Worker + PWA でオフライン対応 + ホーム画面追加可能

技術スタック:

  • フロント: Vanilla JS + Chart.js(フレームワーク不使用)
  • デプロイ: Cloudflare Pages(GitHub連携、無料枠)
  • CMS(ブログ): WordPress(別ドメイン、ConoHa)
  • データ保存: localStorage のみ
  • アナリティクス: Google Analytics 4(送信内容を厳密にコントロール)

公開中のアプリ:みらいコンパス / 家計コンパス


なぜ connect-src 'none' にしたか

家計シミュレーションアプリは 個人情報の塊 です。
収入・資産・住宅ローン残高・家族構成 — 流出したら洒落にならない。

でも、ユーザー目線で「安全です 信じてください」と言われても信じる根拠がない。
そこで物理的に:

Content-Security-Policy: default-src 'self';
  script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data:;
  connect-src 'none';            ← ここ
  frame-ancestors 'none'

connect-src 'none' を指定すると、ブラウザは fetch() / XHR / WebSocket / EventSource を全部ブロックします。
仮に悪意あるコードが混入しても、データを外部に送ることがそもそもできない。

ユーザーは DevTools の Network タブを見れば、ファイルダウンロード以外の通信が一切ないことを自分で確認できます。


アーキテクチャ

[ブラウザ]
   ↓ HTTP(静的ファイル取得のみ)
[Cloudflare Pages]
   - HTML / JS / CSS / SVG
   - Service Worker (キャッシュ戦略)
   - manifest.json (PWA)

[localStorage]
   - lifeplan_v1: ライフプランデータ
   - spending_v1: 支出管理データ
   - 一切外部に出ない

[GA4 (例外)]
   - 流入計測のみ
   - app_open / utm_* / app_name のみ送信
   - 家計データは含めない

WordPress側(lifeplan-app.net)は完全に別物:

  • ブログ・LP・プライバシーポリシー等
  • アフィリエイトCTAはWordPress側に配置
  • アプリ本体(app.lifeplan-app.net)からは外部リンクで遷移

"GA4 だけ例外" の妥協と設計

100%外部送信ゼロ にすると、リピート率もインストール率も永久にわからない。
事業として続けるためには最低限の計測が必要。

そこで CSP に GA4のドメインだけ穴を開ける:

connect-src https://www.google-analytics.com
            https://*.analytics.google.com
            https://*.google-analytics.com
            https://www.googletagmanager.com;

ただし送信する内容を厳密にコントロール:

// analytics.js
(function () {
  const GA_ID = 'G-XXXXXXXXXX';
  // ... gtag.js 読み込み ...
  
  // 送るのは 流入元 と 操作種別 のみ
  gtag('event', 'app_open', {
    app_name: appName,
    utm_source:   qs.get('utm_source')   || '',
    utm_medium:   qs.get('utm_medium')   || '',
    utm_campaign: qs.get('utm_campaign') || '',
    referrer_host: ...,
  });
  
  // 主要操作(PDF出力 / CSV取込など)も操作名のみ
  document.addEventListener('click', function (ev) {
    // 入力データではなく "操作の種別" だけ送る
    ...
  }, true);
})();

送らないルール:

  • ❌ 入力された家計データ
  • ❌ 個人を特定できる情報
  • ❌ 計算結果の数値
  • ✅ 操作のカテゴリ("export_pdf", "import_csv" のみ)

PWA インストール促進の実装

beforeinstallprompt を捕まえてカスタムバナー:

let deferredPrompt = null;
window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showBanner('android');
});

// iOS Safariは beforeinstallprompt 非対応 → 別UI
if (isIOSSafari) {
  setTimeout(() => showBanner('ios'), 30000);
}

window.addEventListener('appinstalled', () => {
  gtag('event', 'pwa_install');  // GA4にインストール完了を計測
});

XSS対策のtips: バナーUIはinnerHTML使わず createElement + textContent で構築。
たとえ自前のコードでも、innerHTML経由で組み立てると後で誰かが文字列補間してXSSが入る余地ができる。


ハマったところ

1. ConoHa の WAF が CSS コメントを 403 ブロック

WordPress側で CSS に /* */ 形式のコメントが入ってると 403 Forbidden が返ってくる。
これは ConoHa WAF の挙動(攻撃パターン誤検出)。
→ ビルド時に raw.replace(/\/\*[\s\S]*?\*\//g, '') で除去して解決。

2. Cocoon テーマの !important 上書き

<style> タグ内の !important は WordPress kses が除去するが、
インライン style="color:... !important" は kses が保持する。
ダーク背景の見出し色を維持するために、HTML インラインスタイル + JS setProperty('important') の両方併用。

3. Service Worker キャッシュの罠

開発中、Service Worker が古いJSを返し続けて挙動おかしいことに気づくのが遅れた。
→ cache-first を network-first for HTML に変更 + デプロイ時のキャッシュバスター必須。

4. ConoHa WAF が Google Fonts も 403 ブロック

fonts.googleapis.com / fonts.bunny.net も全部弾かれる。
→ システムフォントスタック(-apple-system, BlinkMacSystemFont, 'Hiragino Sans', sans-serif)のみで運用。
結果、ロード高速化+日本語IPAライセンス問題も回避できて結果オーライ。


数字の話

運用コスト:

  • Cloudflare Pages: 0円(無料枠内)
  • WordPress(ConoHa): 月990円
  • 独自ドメイン: 年1,500円
  • → 月額 約1,000円

開発時間:

  • 初期実装: 約3週間(個人開発、夜間と週末)
  • 25記事のブログ執筆: 並行で1ヶ月

学び

  1. 「物理的にできない」設計は、ユーザー説得力が桁違いに高い

    • 「外部送信しません(性善説)」と「CSPで送信が物理的に不可能(性悪説対応)」では信頼度が違う
    • DevTools で誰でも検証できる
  2. WordPress + Cloudflare Pages 分離構成は強い

    • ブログのSEO(WordPress)とアプリの安全性(CSP厳格)を両立
    • 障害ドメインも分離されてリスク分散
  3. 計測も "送る内容を最小化する" 思想が必要

    • 取れるデータを全部取るのではなく、本当に必要なものだけ
    • GDPR / 個人情報保護法 / FIPS 等にも親和性高い

実際の画面

実際のアプリの動作画面です:

家計・資産入力画面
月収・賞与・生活費入力 → 月間キャッシュフロー内訳が即可視化

出口戦略シミュレーション結果
リタイア時の推定資産・資産の持続性・必要な改善まで一画面で。すべてブラウザ内(CSP connect-src 'none')で計算。

関連リンク


参考にしたもの


質問・コメント歓迎です。同じ「個人情報を扱うけど信頼してもらう必要がある」アプリを作ってる方の参考になれば嬉しいです。

#WebApp #PWA #CSP #Cloudflare #Security #JavaScript #個人開発

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?