リクエスト詳細
🐛 バグ報告
対応完了
対象アプリ: MindBloom - マインドマップ&アイデア整理
短縮URL共有時のCSRF検証が常に失敗する(home.phpのCSRFトークン取得ミス)
## 1. 不具合の内容
home.php の `saveShortUrl()` 関数内で CSRF トークンを取得する際、`document.querySelector('meta[name=csrf]')` が存在しない場合に空文字列をフォールバックとして送信しています。しかし実際には `lib.php` の `render_layout()` で `<meta name="csrf" content="...">` は確かに出力されているため、セレクタ自体は通常ヒットします。
**本質的な問題は `pages/api.php` の呼び出し方にあります。** home.php は `fetch('index.php?page=api', ...)` で POST しますが、`index.php` は `session_start()` を呼んでから `lib.php` を require し、その後 `pages/api.php` を include します。一方 `pages/api.php` の冒頭では `csrf_check()` を呼んでいますが、**`pages/api.php` 自体には `session_start()` がなく、`lib.php` の関数に依存しています。** これは index.php 経由で呼ぶため問題ないように見えますが、`editor.php` 内の短縮URL保存フェッチも同様の構造です。
より確実な問題として、**`home.php` の `saveShortUrl` が FormData に CSRF トークンを付与するコードに三項演算子による条件分岐**(`document.querySelector('meta[name=csrf]') ? ... : ''`)があり、meta タグが取得できなかった場合(例:DOMContentLoaded 前の早期実行・他ページからの埋め込み・ブラウザ拡張によるDOM操作)は空文字列が送られ `csrf_check()` で `die('CSRF検証エラー')` が返ります。その場合 `fetch` は HTTP 400 を受け取りますが、`.then(r => r.json())` がそのまま実行されレスポンス本文 `'CSRF検証エラー'` は JSON でないため **`SyntaxError` が throw され `.catch()` へ落ちて「URL生成に失敗しました」トーストが表示されます。**
さらに `editor.php` にも同様の短縮URL保存処理があると考えられ、同じ問題が発生します。
## 2. 根拠・発生しそうな条件
- `home.php` の `saveShortUrl()` 内: `fd.append('csrf', document.querySelector('meta[name=csrf]') ? document.querySelector('meta[name=csrf]').content : '');` — meta タグ未取得時に空文字列送信 → CSRF 検証失敗 → JSON でない 400 レスポンス → `r.json()` で SyntaxError → catch で「URL生成に失敗しました」
- マップデータが 8000 バイトを超えるケース(ノードが多いマップ)で初めて短縮URL保存が呼ばれるため、通常利用でも比較的容易に踏む
- `pages/api.php` が `header('Content-Type: application/json')` を送出しているが、`csrf_check()` の `die('CSRF検証エラー')` はプレーンテキストを返すため Content-Type 不一致も重なる
## 3. 期待動作
- 短縮URL保存が成功し「短縮URLをコピーしました!」トーストが表示される
- CSRF 検証エラー時もユーザーに分かりやすいメッセージが表示される
## 4. 修正方針
### (A) `pages/api.php` の CSRF 検証失敗時レスポンスを JSON に統一
```php
// csrf_check() を直接呼ばず、api.php 内でインライン検証してJSONエラーを返す
if (($_POST['csrf'] ?? '') !== ($_SESSION['csrf'] ?? '-')) {
http_response_code(400);
echo json_encode(['error' => 'CSRF error']);
exit;
}
```
### (B) JS 側の fetch エラーハンドリングを改善
```javascript
fetch('index.php?page=api', { method:'POST', body:fd })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => { ... })
.catch(() => showToast('URL生成に失敗しました'));
```
### (C) CSRF トークン取得を確実に
```javascript
const csrfMeta = document.querySelector('meta[name="csrf"]');
fd.append('csrf', csrfMeta ? csrfMeta.content : '');
```
(すでにこの形だが、ページ読み込み完了後にのみ `saveShortUrl` が呼ばれることを確認。DOMContentLoaded 後の呼び出しであれば meta は必ず存在するため、根本的には (A)(B) の修正が優先)
`home.php` と `editor.php` の両方の短縮URL保存処理に対して上記修正を適用すること。
home.php の `saveShortUrl()` 関数内で CSRF トークンを取得する際、`document.querySelector('meta[name=csrf]')` が存在しない場合に空文字列をフォールバックとして送信しています。しかし実際には `lib.php` の `render_layout()` で `<meta name="csrf" content="...">` は確かに出力されているため、セレクタ自体は通常ヒットします。
**本質的な問題は `pages/api.php` の呼び出し方にあります。** home.php は `fetch('index.php?page=api', ...)` で POST しますが、`index.php` は `session_start()` を呼んでから `lib.php` を require し、その後 `pages/api.php` を include します。一方 `pages/api.php` の冒頭では `csrf_check()` を呼んでいますが、**`pages/api.php` 自体には `session_start()` がなく、`lib.php` の関数に依存しています。** これは index.php 経由で呼ぶため問題ないように見えますが、`editor.php` 内の短縮URL保存フェッチも同様の構造です。
より確実な問題として、**`home.php` の `saveShortUrl` が FormData に CSRF トークンを付与するコードに三項演算子による条件分岐**(`document.querySelector('meta[name=csrf]') ? ... : ''`)があり、meta タグが取得できなかった場合(例:DOMContentLoaded 前の早期実行・他ページからの埋め込み・ブラウザ拡張によるDOM操作)は空文字列が送られ `csrf_check()` で `die('CSRF検証エラー')` が返ります。その場合 `fetch` は HTTP 400 を受け取りますが、`.then(r => r.json())` がそのまま実行されレスポンス本文 `'CSRF検証エラー'` は JSON でないため **`SyntaxError` が throw され `.catch()` へ落ちて「URL生成に失敗しました」トーストが表示されます。**
さらに `editor.php` にも同様の短縮URL保存処理があると考えられ、同じ問題が発生します。
## 2. 根拠・発生しそうな条件
- `home.php` の `saveShortUrl()` 内: `fd.append('csrf', document.querySelector('meta[name=csrf]') ? document.querySelector('meta[name=csrf]').content : '');` — meta タグ未取得時に空文字列送信 → CSRF 検証失敗 → JSON でない 400 レスポンス → `r.json()` で SyntaxError → catch で「URL生成に失敗しました」
- マップデータが 8000 バイトを超えるケース(ノードが多いマップ)で初めて短縮URL保存が呼ばれるため、通常利用でも比較的容易に踏む
- `pages/api.php` が `header('Content-Type: application/json')` を送出しているが、`csrf_check()` の `die('CSRF検証エラー')` はプレーンテキストを返すため Content-Type 不一致も重なる
## 3. 期待動作
- 短縮URL保存が成功し「短縮URLをコピーしました!」トーストが表示される
- CSRF 検証エラー時もユーザーに分かりやすいメッセージが表示される
## 4. 修正方針
### (A) `pages/api.php` の CSRF 検証失敗時レスポンスを JSON に統一
```php
// csrf_check() を直接呼ばず、api.php 内でインライン検証してJSONエラーを返す
if (($_POST['csrf'] ?? '') !== ($_SESSION['csrf'] ?? '-')) {
http_response_code(400);
echo json_encode(['error' => 'CSRF error']);
exit;
}
```
### (B) JS 側の fetch エラーハンドリングを改善
```javascript
fetch('index.php?page=api', { method:'POST', body:fd })
.then(r => {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(data => { ... })
.catch(() => showToast('URL生成に失敗しました'));
```
### (C) CSRF トークン取得を確実に
```javascript
const csrfMeta = document.querySelector('meta[name="csrf"]');
fd.append('csrf', csrfMeta ? csrfMeta.content : '');
```
(すでにこの形だが、ページ読み込み完了後にのみ `saveShortUrl` が呼ばれることを確認。DOMContentLoaded 後の呼び出しであれば meta は必ず存在するため、根本的には (A)(B) の修正が優先)
`home.php` と `editor.php` の両方の短縮URL保存処理に対して上記修正を適用すること。
💬 返信 (3)
🛠 開発を開始しました (バグ修正 (mindbloom))
ご要望ありがとうございます。AI 開発ワーカーが実装を開始します。
通常 5〜30 分で Pull Request を作成し、レビュー後にリリースされます。
ご要望ありがとうございます。AI 開発ワーカーが実装を開始します。
通常 5〜30 分で Pull Request を作成し、レビュー後にリリースされます。
📝 開発が完了しました
ご要望いただいた内容の実装が完了し、最終チェック段階に入りました。
レビュー (自動) → リリース、の流れで進みます。
もう少々お待ちください。
ご要望いただいた内容の実装が完了し、最終チェック段階に入りました。
レビュー (自動) → リリース、の流れで進みます。
もう少々お待ちください。
✅ リリース完了のお知らせ
ご要望いただいた「MindBloom - マインドマップ&アイデア整理」を実装し、リリースいたしました。
【ご利用方法】
ダッシュボード: https://www.aiapps.jp/?action=dashboard
アプリ詳細: https://www.aiapps.jp/apps/show.php?slug=mindbloom
デモ環境は 1 時間以内に自動構築されます:
https://www.aiapps.jp/demo/mindbloom/
ご利用ありがとうございます!
ご要望いただいた「MindBloom - マインドマップ&アイデア整理」を実装し、リリースいたしました。
【ご利用方法】
ダッシュボード: https://www.aiapps.jp/?action=dashboard
アプリ詳細: https://www.aiapps.jp/apps/show.php?slug=mindbloom
デモ環境は 1 時間以内に自動構築されます:
https://www.aiapps.jp/demo/mindbloom/
ご利用ありがとうございます!
Echo
Iris