# 青色番頭 —— セキュリティ検査の記録

**版 1.0 ／ 検査日 2026-09-06 ／ 対象 https://aoirobantou.com**

この文書は、青色番頭に対して実際に行った検査の内容と、その出力そのものを公開するものです。
**生の出力を2つ、加工せずに置いてあります**（検査用のトークンを1行だけ取り除いています）。

- [検査の出力 — データベース側（JSON・33KB）](https://aoirobantou.com/docs/audit-2026-09-06-db.json)
- [検査の出力 — API 側（テキスト・73行）](https://aoirobantou.com/docs/audit-2026-09-06-api.txt)

---

## 0. 先に、この検査を誰がやったか

**運営者の指示で、AI（Claude）が実施しました。第三者機関の監査ではありません。**
検査の**出力を、下に全部置いてあります**（DB 側の JSON と API 側のテキストの2本）。検査に使ったプログラムそのものは公開しておらず、運営者の手元にあります。
「誰がやったか」ではなく「何をどう測って、何が出たか」で判断していただくための文書です。

---

## 1. 受けていないもの

はっきり書いておきます。次のものは**ありません**。

| | 状態 |
|---|---|
| ISMS（ISO/IEC 27001）・SOC 2 などの認証 | 取得していません |
| 第三者機関による脆弱性診断 | 受けていません |
| 稼働率の保証（SLA） | 約束していません |
| 復旧時間の目標（RTO）・復旧地点の目標（RPO） | 定めていません |
| 国税庁の認定・JIIMA 認証 | 受けていません |

### 運営者の考え（判定の材料としてお読みください。事実の主張ではありません）

認証は「仕組みが整っていることを第三者が確かめた」という証明で、それ自体は有用です。
ただし取得と維持には継続的な費用がかかり、いまの価格（年額 5,980 円・予定）では、その費用を
お使いになる方に負担していただくことになります。

そこで、**認証の代わりに「何をどう測って、何が出たか」を丸ごと出す**形にしました。
下の §4 には、検査が「問題あり」と印を付けた 12 件も、1 件ずつ理由を書いて残してあります。
都合の悪い行を消していないことは、生の出力と突き合わせれば確かめられます。

個人事業主の帳簿を預かるうえで必要と考える水準は満たしていると考えていますが、
**これは運営者の判断です。**下の数字と生の出力を見て、ご自身で判断してください。

---

## 2. どこで動いているか

**自宅のパソコンや、自前で用意した機材では動いていません。**
帳簿の置き場所も、アプリを動かす場所も、メールを送る仕組みも、専門の事業者に委託しています。

| 何を | どこに | 事業者の所在 | 保存地域 |
|---|---|---|---|
| データベース（帳簿・仕訳・利用者の情報） | **Supabase, Inc.** | 米国 | **日本国内（東京）**。ただし同社の要員による運用・障害対応の際には日本国外から取り扱われることがあります |
| アプリの実行・原本ファイルの保存・配信・自動応答判定・証憑の読み取り | **Cloudflare, Inc.** | 米国 | 実行は世界各地の拠点（**日本国外で行われることがあります**）。原本ファイルの保存領域はアジア太平洋 |
| 確認コードと通知メールの配信 | **Resend** | 米国 | 米国 |
| 有料プランの決済（2026年9月26日 以後） | **Stripe** | 米国 | 米国。**カード番号は運営者に渡りません**（同社の決済ページで直接入力していただきます） |

委託先はこの4社だけです。**広告・アクセス解析・エラー監視の事業者は利用していません。**
一覧はプライバシーポリシー第6項にあり、[/privacy](https://aoirobantou.com/privacy) から読めます。

### これで消える不安と、消えない不安

**消えるもの** —— 自前の機材にまつわる止まり方です。停電、機材の故障、回線の断線、
置いてある部屋への物理的な立ち入り。これらは委託先の設備の問題になります。

**消えないもの** —— **委託先が止まれば、青色番頭も止まります。**
稼働率の保証（SLA）は約束していないので、止まっている間は待っていただくことになります。
また、**委託先はいずれも米国の事業者です。**帳簿そのものの保存地域は東京ですが、運用や障害対応の際には日本国外から取り扱われることがあります。Cloudflare での処理と Resend・Stripe の取扱いも日本国外で行われます。
これを避けたい方には向きません。

帳簿そのものについては、**止まっている間に手元へ出せることが大事**だと考えています。
そのため、いつでも完全な控え（ZIP）を書き出せるようにしてあり、
書き出したものは青色番頭が無くても読める形式（CSV と PDF）にしてあります。

---

## 3. 何を、どう測ったか

検査は、移行 0077 まで入れた**手元のデータベース**と、
そこに繋いだ**本物のアプリ**に対して行いました。本番のデータには触っていません。

★検査した版と、いまの本番の版★ 検査した 2026年9月6日の時点で、本番のスキーマは 0077 でした。同じ日の夜に 0081 まで進んでいます（消費税の貸倒れの回収、書類の受領日と撮影日時、画像の解像度の下限）。0078 から 0081 は、この検査が見た事業者どうしの分離の仕組み（行レベルセキュリティ、関数の会員確認、ロールの権限）を変えるものではありませんが、**この記録はその前の版を測ったもの**です。

**中心にある問いは1つです —— 「A さんの画面から、B さんの帳簿が見えるか」。**
これを、層を変えて4通りの方法で測っています。1つの層が破れても次で止まるか、を見るためです。

| 道具 | 何をするか | 規模 |
|---|---|---|
| **データベースの棚卸し** | 全部の表について、行レベルセキュリティ（RLS）が効いているか、どの条件で行が見えるかを、カタログから読み出す | 表 56 件 |
| **関数への総当たり** | データベースの関数のうち、事業者 ID を引数に取るもの全部に、**他人の事業者 ID** を渡して呼ぶ | 関数 86 本 |
| **数え上げ** | A のログイン状態のまま、B の事業者 ID をヘッダに載せて、全部の表の行を数える | 表 53 件 |
| **HTTP の検査** | 実際に起動したアプリへ、**A のトークンと B の事業者 ID の組み合わせ**で API を叩く | 70 項目 |

---

## 4. 結果

### 数字

| 検査 | 結果 |
|---|---|
| HTTP 70 項目 | **70 件すべて通過・失敗 0** |
| 表 56 件の RLS | ok 52 ／ 全行ポリシー 3 ／ RLS 無し 1（→ §4.1） |
| 関数 86 本への他人の ID | 守られた 80 ／ 例外を投げなかった 6（→ §4.2） |
| 数え上げ 53 件 | ok 51 ／ 印が付いた 2（→ §4.3） |
| **PUBLIC が実行できる関数** | **0 本** |
| Supabase の既定ロール（anon・authenticated・service_role）が `app` / `app_auth` / `app_secure` を使えるか | **3 ロール × 3 スキーマ = 9 通りすべて不可** |

**他人のデータが1件でも見えた箇所は、ありませんでした。**

以下は、検査の道具が「問題あり」と印を付けた 12 件です。**印が付いた理由を1件ずつ書きます。**

### 4.1 全行ポリシー 3 件・RLS 無し 1 件

| 表 | 中身 | なぜ全員が読めてよいか |
|---|---|---|
| `app.account_templates` | 勘定科目の雛形 | 事業者の列がありません。全員が同じ雛形から始めます |
| `app.system_versions` | スキーマの版 | 同上。番号だけです |
| `app.cron_runs` | 定期処理の実行記録 | 同上。ただし下に注記があります |
| `public.schema_migrations` | 移行の台帳 | 適用済みの移行ファイル名だけ。利用者のデータはありません |

**`app.cron_runs` については、そのままでは不十分です。**
この表の `detail` 欄には、処理の内容によって事業者の識別子が入ることがあります
（例：「鍵が無いので、ある事業者を飛ばした」という記録に、その事業者の識別子が入ります）。
表そのものは全員が読める設計なので、**画面に出す側で絞っています** ——
出すのは `warnings` の欄だけで、しかも **UUID の形やメールアドレスの形を含む行は落とします**。
実装は `src/shared/cron-warnings.ts` にあり、将来この欄に事業者の情報を入れる変更が入っても、
そこで落ちるようにしてあります。

### 4.2 例外を投げなかった関数 6 本

この道具は「他人の事業者 ID を渡して**例外が飛ばなかったもの**」に印を付けます。
例外が飛ばなくても、**返ってきた中身が他人のものでなければ問題ではありません。** 6 本ともそれに当たります。

| 関数 | 返したもの | なぜ問題ないか |
|---|---|---|
| `app.is_member` | `false` | 「会員か」を答える関数です。`false` を返すのが正しい守りで、例外を投げるほうが不自然です |
| `app.is_writable` | `true` | 「その事業者が凍結されていないか」を答える関数で、**会員かどうかとは別の問い**です。会員確認は `assert_member` が別に行います |
| `app.assert_writable` | 何も返さない | 同上。凍結されていなければ通します |
| `app.consumption_tax_rows` | **0 行** | 条件に `and app.is_member(p_tenant)` が入っています。会員でなければ 1 行も返りません |
| `app.storage_usage` | すべて 0 | 呼んだ人の権限で走るので、RLS により他人の書類が 1 件も見えず、合計が 0 になります |
| `app.storage_settings_for` | 既定値（2400px・品質 85・2GB） | 他人の設定ではなく、**関数の中に書いてある既定値**です。他人の行が見えないので既定値に落ちます |

### 4.3 「LEAK」と印が付いた 2 件

`app.tenant_lifecycle` と `app.tenant_members` の 2 件です。**これは道具の判定が粗いために出た印です。**

この道具は「**A が B のヘッダを付けて数えたとき、0 件より多ければ LEAK**」と判定します。
A には**A 自身の行**が見えるので、1 件と数えられて印が付きました。

同じ行に、もう1つの数字が併記してあります。**`seenByA_filteredB`（B の行だけを数えたもの）で、2 件とも 0 です。**

```
{"table":"app.tenant_lifecycle","ownRowsAsB":1,"seenByA_withBHeader":1,"seenByA_filteredB":0,"verdict":"LEAK"}
{"table":"app.tenant_members", "ownRowsAsB":1,"seenByA_withBHeader":1,"seenByA_filteredB":0,"verdict":"LEAK"}
```

**B の行は 1 件も見えていません。**
判定を後から書き換えて緑にすることもできましたが、**道具の出力はそのまま残してあります。**

---

## 5. 同じ日に直したもの

この検査で見つかり、2026-09-06 に直したものです。すべて本番に反映済みです。

| 見つけたこと | 直し方 |
|---|---|
| **暗号化されていない `http://` でも全画面が返っていた**（`https://` へ転送していなかった） | 本番では `http:` で来た要求を 301 で `https:` へ回すようにしました。ドメイン側でも常時 HTTPS を有効にしました |
| ログインしていなくても叩ける経路（記録印の照合・共有 URL）の回数制限が、アプリの中にしか無かった | 手前のネットワーク側にも回数制限を置きました |
| **ログアウトしてもサーバー側でトークンが失効していなかった**（90 日間有効なまま） | すべての端末のログインを失効させる経路を作り、データベース側で失効させるようにしました |
| 日次処理が途中で打ち切られると、記録も通知も残らなかった | 最終成功時刻を外部の監視から見えるようにしました |
| 他人の年度 ID を渡すと 500（サーバーエラー）が返っていた | 404（見つかりません）を返すようにしました |
| 事業者 ID を受け取るのに会員確認をしていない関数が 7 本あった | 7 本に会員確認を追加しました |

`http://` が転送されることは、次のコマンドでご自身で確かめられます。

```bash
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://aoirobantou.com/intro
```

---

## 6. 直していないもの・見ていない範囲

**残っているものも書きます。**

| | 状態 |
|---|---|
| パスワードの規則 | 8 文字以上のみ。漏えい済みパスワードの一覧との照合はしていません。ただし**ログインにはメールの確認コードが毎回必要**なので、パスワードだけでは入れません |
| 検査の範囲外 | Cloudflare・Supabase・Stripe の管理画面そのもの、および運営者の端末は見ていません |
| 検査の性質 | 攻撃者になりきった侵入テスト（ペネトレーションテスト）ではありません。**設計と実装を読み、境界を機械的に総当たりする**方法です |

**この検査で示せるのは「事業者どうしの分離が効いているか」までです。**
お使いになる方のパスワードやメールが第三者に渡った場合は、この検査の範囲外です。

---

## 7. 生の出力

- [データベース側の出力（JSON）](https://aoirobantou.com/docs/audit-2026-09-06-db.json)
  —— 表 56 件・関数 86 本・数え上げ 53 件・ロールとスキーマの権限。§4 の数字はすべてこのファイルから数えたものです。
- [API 側の出力（テキスト）](https://aoirobantou.com/docs/audit-2026-09-06-api.txt)
  —— 70 項目。最後の行に `70 checks, 0 failed` があります。

API 側の出力の最終行にあった検査用トークンと識別子だけ、公開版では取り除いてあります
（`CTX` で始まる 1 行）。それ以外は道具が出したままです。

---

## 8. この文書について

- 検査日 2026-09-06。**この日以降の変更は反映されていません。**
- 検査に使ったプログラムは運営者の手元にあり、同じ手順で繰り返し実行できます。
- 記載に誤りを見つけられた場合は、[運営者情報](https://aoirobantou.com/operator)の連絡先までお知らせください。
