📋 目次
- 今回のスプリント概要
- セキュリティ監査:8つの問題を発見
- 修正① HSTS + CSP ヘッダーを追加
- 修正② JWT を localStorage から追い出す
- 修正③ XSS パッチ(innerHTML → DOM API)
- 修正④ その他5項目
- UX改善:インストーラー・ログアウト・自動ペアリング
- macOS アプリの署名と公証(notarization)
- まとめ
今回のスプリント概要
ミセバンAI v1 のリリース後、「セキュリティ的に問題ないか調べてほしい」というオーダーが入った。Claude Code で監査を走らせたところ、8つの問題が見つかった。すべて「全部直して」の一言で修正スプリントが始まった。
同時並行で、インストーラーのUX改善・macOS アプリの Gatekeeper 署名・ランディングページからダッシュボードへのメール引き回しなど UX 系の修正もまとめて対応した。
セキュリティ監査:8つの問題を発見
監査の結果、以下の問題が洗い出された。
https://misebanai.com がソースに埋め込まれており、ステージング環境で使えなかった。BASE_URL 環境変数に外出し。POST /api/v1/auth/logout が必要になった。修正① HSTS + CSP ヘッダーを追加
nginx.conf のサーバーブロックに2行追加した。
add_header Strict-Transport-Security
"max-age=63072000; includeSubDomains; preload" always;
add_header Content-Security-Policy
"default-src 'self';
script-src 'self' 'unsafe-inline' 'unsafe-eval'
https://cdn.jsdelivr.net https://www.googletagmanager.com ...;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';" always;
既存コードがインラインスクリプトを多用しているため unsafe-inline は外せなかった。ただし frame-ancestors 'none'(クリックジャッキング防止)・base-uri 'self'・form-action 'self' だけでも十分な防御層になる。
修正② JWT を localStorage から追い出す
ブラウザセキュリティの鉄則として、セッショントークンは HttpOnly Cookie に保持すべきだ。JS が document.cookie で読めないため、XSS が成立しても盗まれない。
Rust 側(auth.rs)に Cookie 発行ヘルパーを追加した。
pub fn session_cookie_value(token: &str) -> String {
format!(
"miseban_session={}; HttpOnly; Secure; \
SameSite=Lax; Path=/api; Max-Age=2592000",
token
)
}
login / signup / magic_login / verify_otp の全4エンドポイントが Set-Cookie ヘッダーを返すように変更。AuthUser エクストラクターは Cookie を優先し、なければ Authorization: Bearer にフォールバックする(後方互換を維持)。
nginx の /api/ リバースプロキシは upstream から来る Set-Cookie をそのままブラウザに転送するため、追加設定なしで動作した。
修正③ XSS パッチ(innerHTML → DOM API)
アラートレンダリングのコードが典型的な XSS 脆弱性を持っていた。
// ❌ 修正前
list.innerHTML = alerts.map(a =>
`<div>${a.message}</div>` // a.message が <script> だったら?
).join('');
// ✅ 修正後
alerts.forEach(a => {
const div = document.createElement('div');
const text = document.createElement('div');
text.textContent = a.message; // textContent は自動エスケープ
div.appendChild(text);
list.appendChild(div);
});
API トークン一覧の onclick="deleteApiToken('${t.id}')" も同様に修正。ID をHTML文字列に直接埋め込む代わりに addEventListener でクロージャに閉じ込めた。
修正④ その他5項目
- JWT 30日化:
chrono::Duration::days(90)→days(30) - OTP レート制限:
HashMap<String, Vec<Instant>>をインメモリで管理。10分ウィンドウで5回超えたら 400 を返す - BASE_URL 環境変数:マジックリンクURLを
std::env::var("BASE_URL")で生成 - logout エンドポイント:
POST /api/v1/auth/logoutがMax-Age=0の Cookie を返してセッションを失効 - apiFetch に credentials: 'include':ブラウザが Cookie を送るために必須
UX改善:インストーラー・ログアウト・自動ペアリング
インストーラー UI
ダウンロードページのエージェントインストール手順が「ターミナルでコマンドを実行」という上級者向けの UI になっていた。これを大きな「DMGをダウンロード」「ZIPをダウンロード」ボタンに変更し、コマンドは <details> の中に折りたたんだ。
ログアウト即反映
「すぐ勝手にログアウトされる」という報告があった。原因は setupSessionCheck() の実装ミスで、API が503を返したとき(Fly.io の sleep from idle)も logout を呼んでいた。
// ❌ 修正前
if (!resp.ok && currentUser) { clearAuth(); }
// ✅ 修正後
if (resp.status === 401 && currentUser) { clearAuth(); }
ネットワークエラーや503は「認証失敗」ではないため、401 のときだけログアウトするように変更した。
ダッシュボードからの自動ペアリング
エージェントが既に起動中であれば、ダッシュボード上の「エージェントに接続」ボタンでローカルエージェント(http://localhost:3939)を直接開き、ペアリングコードを自動入力・自動送信できるようにした。
混合コンテンツ(HTTPS ページから HTTP localhost への fetch)はブラウザにブロックされるため、window.open('http://localhost:3939/?pair=XXXXXX') でナビゲーションとして開き、エージェント側で JS を注入して自動送信する設計にした。
ランディング→ダッシュボードのメール引き回し
トップページのヒーローフォームでメールを入力すると /dashboard/?mode=signup&email=xxx にリダイレクトするが、ダッシュボード側が email パラメータを無視していたため、もう一度メールを入力させられていた。
URLSearchParams で email を読み取り、フォームへの自動入力 + 自動送信(magic link 発行)を実装した。ユーザーはメールアドレスを1回入力するだけでメールボックスに飛べる。
macOS アプリの署名と公証(notarization)
「このアプリケーションにマルウェアが含まれていないことを検証できません」というGatekeeperエラーが報告された。
修正は3段階で行った。
Step 1:Developer ID 署名
codesign --force --deep --options runtime \
--entitlements entitlements.plist \
--sign "Developer ID Application: Yuki Hamada (5BV85JW8US)" \
MisebanAI.app
entitlements には com.apple.security.network.server と network.client を指定(ローカルHTTPサーバーに必要)。
Step 2:Apple 公証(notarytool)
xcrun notarytool submit MisebanAI-notarize.zip \
--keychain-profile "misebanai-notarize" \
--wait
# → status: Accepted
Step 3:シェルスクリプト → Swift バイナリに置き換え
署名・公証後も「開発元を検証できないため開けません」が出続けた。原因は メイン実行ファイルがシェルスクリプトだったこと。Gatekeeper は .app のランチャーが Mach-O バイナリでないと正しく検証できない。
Swift でコンパイル済みのランチャーバイナリを作成し、シェルスクリプトと置き換えた。
swiftc -O main.swift -o MisebanAI \
-framework Foundation -framework AppKit
その後再署名・再公証・ステープルを実施。
spctl --assess --type execute --verbose MisebanAI.app
# → MisebanAI.app: accepted
# → source=Notarized Developer ID ✅
DMG も同様に署名・公証・ステープルして完了。これで Gatekeeper のダイアログは一切出なくなった。
まとめ
今スプリントで対応した内容をまとめる。
| カテゴリ | 対応内容 | 状態 |
|---|---|---|
| セキュリティ | HSTS / CSP ヘッダー追加 | ✅ 完了 |
| セキュリティ | JWT を HttpOnly Cookie へ移行 | ✅ 完了 |
| セキュリティ | innerHTML XSS 修正(アラート・APIトークン) | ✅ 完了 |
| セキュリティ | JWT 30日化・OTPレート制限・logout EP | ✅ 完了 |
| UX | インストーラーUI(DMGボタン化) | ✅ 完了 |
| UX | 誤ログアウトバグ修正 | ✅ 完了 |
| UX | ダッシュボードからの自動ペアリング | ✅ 完了 |
| UX | ランディング→ダッシュボードのメール引き回し | ✅ 完了 |
| macOS | Developer ID 署名 + Apple 公証 + ステープル | ✅ 完了 |
| macOS | シェルスクリプト → Swift バイナリランチャー | ✅ 完了 |
セキュリティ系は「ベータ版だからまあいいか」と後回しにしがちだが、実ユーザーが使い始めた時点で本番だ。今回まとめて対応できてよかった。
次のスプリントは LINE レポートの精度向上と、カメラ映像のリアルタイムプレビュー機能を予定している。