Contriは「事業の数字」というセンシティブな情報をお預かりするサービスです。セキュリティは「実装している」だけでなく、お客様に「見える形で明示」してこそ意味があると考えています。
1. データ保護
暗号化
| 対策 | 方式 | 備考 |
|---|---|---|
| 通信暗号化 | HTTPS(TLS 1.3) | 全通信を暗号化 |
| 保存時暗号化(ディスク) | AES-256 | データベースのディスクレベル暗号化 |
| 外部サービストークン | AES-256-GCM(アプリケーション層) | MoneyForward 等の OAuth トークンを当社秘密鍵で再暗号化。DB ディスク暗号化と合わせた二重防御により、万が一の DB ダンプ流出時も復号には当社秘密鍵が必要 |
| レシート画像の Storage 非保存 | Gemini API への直送のみ | レシート画像は OCR (光学文字認識) 処理時のみ Gemini API に base64 で直送します。当社サーバー上の Supabase Storage には永続保存せず、署名付き URL も発行しません。URL 流出による不正アクセスや、Storage 上の orphan ファイルが残存する経路を構造的に排除しています |
| パスワード | bcryptハッシュ | 復号不可能。当社スタッフも閲覧不可 |
アクセス制御
| 対策 | 詳細 |
|---|---|
| Row Level Security(RLS) | 全テーブルでユーザーデータを完全隔離 |
| 二段階認証(2FA) | TOTP(Google Authenticator等)対応 |
| パスワードポリシー(オーナーアカウント) | NIST SP 800-63B Rev4 準拠。最低8文字、ASCII印字可能文字。MFA併用時の十分性確保を前提とし、複雑性ルール(記号必須等)の強制および定期変更の強制は行わない(NIST 最新推奨) |
| メンバーアカウントのパスワード要件 | タイムカード PWA のメンバーアカウントは最低 10 文字を要求。オーナーが招待時に強度の高いパスワードを設定することで、リスト型攻撃・推測攻撃に対する耐性を確保 |
| パスキー(WebAuthn) | 中期ロードマップとして対応検討中。フィッシング耐性を持つ FIDO2 認証への段階的移行を予定 |
| 漏洩パスワード検出 | HaveIBeenPwned連携によりリスト型攻撃で既知の漏洩パスワード使用を防止 |
| APIレート制限 | AI機能: 20リクエスト/分/ユーザー |
| Server Action の境界制御 | 内部呼び出し用関数は 'use server' ファイルから export せず、internal helper として分離。Server Action manifest 経由で外部から任意パラメータで管理者クライアントが呼び出されるクロステナント読取リスクを構造的に防止 |
| Origin 検証(CSRF 多層防御) | POST 系の主要 API(Stripe Portal / Stripe Checkout / AI Chat / AI Tool 確認 / メンバー認証)に Origin allowlist 検証を実装。SameSite Cookie 単独依存からの脱却 |
| 外部サービス連携 | OAuth 2.0による認可。HMAC署名付きstateパラメータ + httpOnly Cookie nonceでCSRF防止。トークンはサーバーサイドのみ保持(フロントエンドに一切渡さない)。読み取り専用スコープのみ使用 |
| パスワード変更時の再認証 | 最終ログインから24時間経過時は再ログインを要求(セッション乗っ取り対策) |
| パスワード変更時の現パスワード確認 | 変更時に現在のパスワード入力を必須化(攻撃者がセッションを盗んでも変更不可) |
| メンバーアカウントのセッション維持 | タイムカードPWAは access JWT(24時間) + 不透明 refresh token(30日)の二段構え。refresh token は SHA256 ハッシュのみDB保存し、利用ごとに自動更新(rotation)。明示ログアウト・不正検知時はオーナーが全端末で即時無効化可能 |
| 退会フローの単純化と論理削除管理 | Free ユーザーは設定画面の確認モーダルでチェックボックスに同意して退会し、退会完了メールで通知します。Pro / Ultra は Stripe Customer Portal で解約し、Free 降格後に退会します。退会後 30 日間は account_deletion_records と account_deletion_locks により論理削除状態としてログインと通常アクセスを遮断し、support@contri.jp 経由で復元できます。30 日経過後は冪等な finalizer が DB / Auth ユーザーの物理削除を実行します(レシート画像は当社サーバー上に永続保存しない設計のため finalizer の削除対象には含まれません) |
通信セキュリティ
| 対策 | 詳細 |
|---|---|
| Content Security Policy(CSP) | XSS攻撃を防止する厳格なポリシーを設定 |
| X-Frame-Options | DENY — クリックジャッキング攻撃を完全に防止 |
| X-Content-Type-Options | nosniff — MIMEタイプスニッフィングを防止 |
| Referrer-Policy | strict-origin-when-cross-origin — リファラー情報の漏洩を最小化 |
| Permissions-Policy | カメラ・マイク・位置情報へのアクセスを無効化 |
デプロイメント保護
| 対策 | 詳細 |
|---|---|
| Preview デプロイメント認証 | 本番以外のデプロイメント(開発中機能の確認URL)はVercel認証必須。第三者がURLを入手してもアクセス不可 |
2. インフラストラクチャ
| サービス | 認証 | データ所在地 |
|---|---|---|
| Supabase | SOC 2 Type II + ISO/IEC 27001:2022 | 東京 |
| Vercel | SOC 2 Type II | 東京 |
| Stripe | PCI DSS Level 1 | セキュアサーバー |
毎日自動バックアップ(7日分保持)。Point-in-Time Recovery(PITR)対応可能(必要に応じて有効化)。
ソースコード・シークレット管理
近年、同業の SaaS 事業者でも GitHub アカウントへの不正アクセス起因のインシデントが発生しています。当社はソースコードリポジトリと API キー・秘密鍵の管理を分離し、いずれか一方の流出だけでは本番データに到達できない設計としています。
| 対策 | 詳細 |
|---|---|
| 環境変数のSensitive保管 | 全APIキー・秘密鍵はVercel Sensitive Environment Variablesで格納。当社スタッフおよびVercelスタッフも閲覧不可。ソースコード内には一切ハードコードしない |
| GitHub組織のMFA必須化 | 当社GitHub組織の全メンバーに対してMFAを必須化。リポジトリへの直接 push は Branch Protection により制限 |
| シークレットスキャン | GitHub Secret Scanning による push 前検知、ならびに Dependabot による依存パッケージ脆弱性監視を有効化 |
| サーバー専用モジュールの分離 | データベース管理権限を持つコード(Service Role キー使用)、決済 SDK(Stripe)、AI 補助モジュール(会話タイトル生成等)に import 'server-only' 制約を課し、クライアント側コンポーネントへの誤 import をビルド時に検知・防止 |
| キーローテーション | 漏洩・不審兆候発見時は速やかに新キー発行・旧キー即時無効化。定期的なローテーションも実施 |
| 業界事例の継続監視 | 同業 SaaS のインシデント公表を常時参照し、適用可能な再発防止策を当社運用に取り込む |
エラーログの秘匿情報マスク
エラー監視(Sentry)に送信される情報から、以下の領域を再帰的にスキャンし、秘匿情報を自動マスクします(深さ 8 段を超える深部や循環参照は fail-closed で [Filtered-DepthLimit] / [Filtered-Circular] に置換):
- HTTP ヘッダー(Authorization / Cookie / x-api-key 等)
- リクエストボディ(request.data)
- 追加コンテキスト(extra / tags / contexts)
- エラーメッセージ本文(event.message)
- Breadcrumbs(ユーザー操作履歴)
- Sentry 標準 user context(
event.userをidのみ保持し他キーは構造的に削除、Issue #176 / v2.89.1)
マスク対象は API キー様文字列(sk-* / xoxb-* / JWT 等)、認証トークン、秘密鍵、決済関連の機微情報を含みます。Sentry.captureException(err, { extra: { token: "sk_live_..." } }) のように開発者が意図せず仕込みうる流出経路も網羅的に塞ぐ設計です。
加えて、AI 応答の数値整合性検証(numeric-parity-verifier)によって検出された数値・単位の不一致情報を Sentry に送信する際は、tool 内部の数値・単位・キー名フィールドのみに限定し、会話本文(response の raw 文字列)は意図的に除外することで第三者送信時の個人情報露出を最小化しています(Issue #151 / v2.74.0)。また、Sentry 通報にユーザーID(Supabase Auth UUID)を Sentry 標準 user context として送信することで運営側の障害トリアージ実効性を維持しつつ、PII を user context へ隔離する構造改善を実施しています(Issue #150 / v2.73.0)。
3. AI機能のデータフロー
✓送信されるデータ
- 仕訳データ(金額、勘定科目名)
- お客様の質問文
- レシート画像(OCR利用時のみ、当社サーバー上には永続保存せず Gemini API へ base64 で直送)
✕送信されないデータ
- パスワード
- メールアドレス
- クレジットカード情報
- 他のユーザーのデータ
Google・OpenAI・Anthropicともに、API経由で送信されたデータをAIモデルの学習に使用しません。AI機能の利用は任意です。
AIリサーチ機能の外部サービス
AIが業界情報・最新トレンド等の調査を必要と判断した場合、以下の検索・取得系APIを利用します。
Tavily(米国)
AI最適化されたWeb検索API
- 送信: 一般的な検索クエリのみ
- 非送信: 仕訳データ・個人情報
- APIキー認証による通信
Jina AI(シンガポール)
公開WebページのMarkdown変換API
- 送信: AIが選択した公開URL
- 非送信: 仕訳データ・個人情報
- HTTPS通信、5分キャッシュで効率化
これらのリサーチAPIには、ユーザーの仕訳データ・個人情報は送信されません。AIが質問から抽出した一般的な検索語(例:「SaaS業界 CPH 平均」)と、取得対象の公開URLのみを送信します。
AIセキュリティ対策(OWASP LLM Top 10 / IPA 10大脅威 2026 対応)
IPA 情報セキュリティ10大脅威 2026 において「AIの利用をめぐるサイバーリスク」が組織編 3 位に初登場するなど、生成 AI 利用に固有のリスクへの対応が事業者の責務となっています。当社は OWASP Top 10 for LLM Applications および OWASP Top 10 for Agentic Applications 2026 を設計時の参照基準とし、以下の多層防御を実装しています。
| 対策 | 詳細 |
|---|---|
| プロンプトインジェクション対策 | ユーザー入力とシステムプロンプトを明確に分離し、ユーザー入力に含まれた指示で AI の挙動方針が書き換えられない設計。出力もポストプロセスで検証 |
| ツール実行のスコープ制限 | AI が呼び出せる Function Calling ツールは、現在ログイン中のユーザー自身のデータに対する読み取り・書き込みのみに制限。他ユーザーのデータには Row Level Security(RLS)により到達不可能 |
| AIリサーチの読み取り専用境界 | AIリサーチ機能(Tavily / Jina AI)は外部公開 Web の読み取りに限定。AI が外部サービスへ書き込みを行ったり、ユーザーの認証情報を持ち出すことはできない構造 |
| 送信前マスク | パスワード・メールアドレス・クレジットカード情報は AI プロバイダーへの送信パイプライン到達前にフィルタリング |
| 応答品質の3層検証 | システムプロンプト・ツール実行結果・ポストプロセスの3層で AI 応答の品質と整合性を担保。LLM の直近コンテキストへの過度な引きずられを防止 |
| レート制限 | AI 機能の濫用・コスト枯渇攻撃を防ぐため、ユーザー単位のレート制限を実装(後述「APIレート制限」セクション) |
| 会話履歴のロール検証 | AI に渡す会話履歴の各メッセージについて、ロール(user / assistant)を実行時に allowlist 検証。攻撃者が「role: system」を仕込んで本来のシステムプロンプトを書き換えるインジェクション攻撃を防止 |
| 予算予約のプロバイダー別最適化 | AI 呼出時の出力トークン予算予約量を Google / OpenAI / Anthropic の max_tokens 上限に合わせて分岐し、原価上限を超過する経済攻撃リスクを抑制。提供事業者間のハンドオフ時にも同基準を再適用 |
| 外部 Web 取得結果の隔離 | AI リサーチで取得した外部 Web コンテンツは「外部入力」として明示的にマーク(taint)し、その内容を直接根拠とした書き込み系ツール呼び出しは制限する設計を採用 |
| 内部システム情報の漏洩防止 | AI 機能でツール実行中にエラーが発生した場合、その内部詳細(サーバー内ファイルパス・モジュール名・スタックトレース)をお客様画面には表示しません。エラー発生源での一般化、データ送信経路での再帰的な内部パス除去、AI モデルへの応答指針の 3 層構成で、内部システム構造の露呈を防ぐ多層防御を実装 |
4. 決済セキュリティ
クレジットカード情報はStripeが直接処理します。当社のサーバーを経由しません。StripeはPCI DSS Level 1(決済セキュリティの最高基準)に準拠しています。
2025年3月31日以降、PCI DSS は v4.0 への完全移行が必須となりました。当社は Stripe Checkout を採用しており、カード番号・有効期限・セキュリティコードはお客様の端末から Stripe のセキュアサーバーへ直接送信され、当社のサーバーやデータベースを一切経由しません。当社が保持するのは Stripe から発行される顧客ID・サブスクリプションIDのみで、これは PCI DSS の SAQ A(最も低リスクなマーチャント区分)に該当する構成です。
無料トライアルの重複付与防止
初回登録時の 30 日無料トライアル(Pro 月額プラン)に対して、以下の 2 層判定で重複付与を防止しています。
- trial_history テーブル: ユーザー登録メールアドレスを正規化(Gmail の dot-trick・+alias を平準化)して履歴を蓄積
- Stripe Customer / Subscription 履歴: 過去に同一 Stripe 顧客がトライアルを利用していないかを Stripe API で確認
いずれか一方で過去のトライアル利用が検出された場合、再付与を行いません。両方の判定を全 Checkout エントリポイントで共通利用することで、利用規約「アカウントの単一性」と整合した運用を実現しています。
さらに、Stripe webhook 経由で記録されたトライアル履歴 (`trial_history` テーブル) は、毎月 1 日 09:00 JST に Vercel Cron (`/api/cron/trial-monitoring`) が同一カード × 別アカウントの重複を検知し、当社運用者に通知メールを送信します。検知結果に基づく判断・是正は当社運用者の手動オペレーションで行われ、自動的なユーザー権限変更は行いません。また、データベース問合せエラー時はトライアル付与を停止する fail-closed 設計を採用し、エラー時の誤付与を防止しています。
5. メール認証(送信ドメイン認証)
| 仕組み | 目的 |
|---|---|
| SPF | 当社が contri.jp ドメインから送信するメールの送信元IPを認証DNSに公開し、なりすまし送信を防止 |
| DKIM | 送信メールに当社秘密鍵による電子署名を付与。受信側で改ざんとなりすましを検証可能 |
| DMARC | SPF・DKIMの検証結果に基づいて受信側に処理ポリシー(拒否・隔離・通過)を指示するDNSレコードを公開 |
contri.jp ドメインから送信される認証メール・通知メールは、これら3つの送信ドメイン認証技術により真正性が保証されています。
6. データベース関数のセキュリティ
PostgreSQL のストアドファンクション(データベース関数)に対し、以下の対策を実施しています。
| 対策 | 詳細 |
|---|---|
| 検索パスの固定 | すべてのDB関数に SET search_path = public を明示的に設定し、スキーマ汚染攻撃を防止 |
| SECURITY DEFINER の最小権限 | 権限昇格が必要な関数のみ SECURITY DEFINER を付与し、関数本体内で auth.uid() 等を用いて呼び出し主体を厳密に検証 |
| 退会 finalizer の lease/CAS | account_deletion_records の claim_owner / claim_until / attempt_count を使い、30 日経過後の物理削除を FOR UPDATE SKIP LOCKED と lease/CAS で直列化します。DB / Auth ユーザーの各 step は冪等に再実行でき、24 時間を超えても完了しない稀なケースでは Sentry 通知を受けた当社運用者が個別に対応します(レシート画像は当社サーバー上に永続保存しない設計のため finalizer の物理削除対象には含まれず、過去の legacy データは専用の cleanup スクリプトで対応します) |
| 退会 RPC の atomic 実行 | delete_user_account_atomic は advisory lock の下で実行し、DB 物理削除を単一トランザクションに集約します。通常退会時は呼び出さず、30 日経過後の finalizer または support@contri.jp 経由の即時抹消処理だけが使用します |
| Supabase Security Advisors | Supabase 提供のセキュリティ警告(advisors)を定期的に確認し、WARN レベルの指摘がある場合は速やかに是正 |
7. APIレート制限
不正な大量リクエストおよびコスト枯渇攻撃(Denial of Wallet)からサービスを保護するため、ユーザー単位のレート制限を設定しています。
| エンドポイント | 上限 |
|---|---|
| AIチャット | 20 リクエスト/分/ユーザー |
| レシートOCR | 10 リクエスト/分/ユーザー |
| 決済(Stripe Checkout) | 3 リクエスト/分/ユーザー |
上限超過時は HTTP 429 を返却し、ユーザーには再試行までの待機時間を案内します。
8. セキュリティ運用(外部視点の独立検証)
当社のセキュリティ実装は、内部レビューだけでは見落としが起こり得る前提で運用しています。重要な変更・監査時は、AI コーディングアシスタント(社内呼称「サム」)に加えて、独立した第二の AI コードレビュワー(社内呼称「コーさん」)を並行起動し、それぞれが独立にコードベースを検査する 相互検証プロセスを採用しています。
独立検査
各 AI に互いのレポートを与えず、双方が独立に脆弱性・齟齬・退行リスクを抽出
相互照合と再検査
双方の結果を突合し、片方しか発見していない指摘・解釈の相違は再検査の対象として深掘り。最低 2 ラウンド、必要に応じて追加ラウンドを実施
両者合意で 100 点判定
双方が「指摘事項なし」で合意した時点を「合格」とし、本番反映する。重要なセキュリティ改修はこのプロセスを通過するまでリリースしません
ローンチ前最終監査(2026-05-21)でも本プロセスを採用し、High 5 件 + Medium 5 件のセキュリティ強化を両者合意で本番反映しました。
9. インシデント対応
万が一、セキュリティインシデントが発生した場合:
初動対応
影響範囲の特定と被害拡大の防止。必要に応じてユーザーセッションの即時無効化・漏洩したAPIキーのローテーションを実施
お客様への通知
影響を受けるお客様に、判明後72時間以内にメールで通知
原因調査
根本原因の調査と再発防止策の策定
監督官庁への報告
個人情報保護委員会への報告(法令に基づき必要な場合)