CSP connect-src 'none' で完結する家計簿Webアプリを作って Cloudflare Pages で運用してる話
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ヶ月
学び
-
「物理的にできない」設計は、ユーザー説得力が桁違いに高い
- 「外部送信しません(性善説)」と「CSPで送信が物理的に不可能(性悪説対応)」では信頼度が違う
- DevTools で誰でも検証できる
-
WordPress + Cloudflare Pages 分離構成は強い
- ブログのSEO(WordPress)とアプリの安全性(CSP厳格)を両立
- 障害ドメインも分離されてリスク分散
-
計測も "送る内容を最小化する" 思想が必要
- 取れるデータを全部取るのではなく、本当に必要なものだけ
- GDPR / 個人情報保護法 / FIPS 等にも親和性高い
実際の画面
実際のアプリの動作画面です:

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

リタイア時の推定資産・資産の持続性・必要な改善まで一画面で。すべてブラウザ内(CSP connect-src 'none')で計算。
関連リンク
- 公開中のアプリ: https://app.lifeplan-app.net/
- WordPress ブログ: https://lifeplan-app.net/blog/
- アプリのプライバシー方針: https://lifeplan-app.net/privacy-policy/
参考にしたもの
- MDN: Content-Security-Policy: connect-src
- web.dev: Add a web app manifest
- Google: Customizing the install experience for PWAs
- Cloudflare Pages docs: Headers file
質問・コメント歓迎です。同じ「個人情報を扱うけど信頼してもらう必要がある」アプリを作ってる方の参考になれば嬉しいです。
#WebApp #PWA #CSP #Cloudflare #Security #JavaScript #個人開発
