グローバル状態に依存する「認可バイパス」の死角と、SSO連携におけるID出所検証の破綻
1. 概要
エンタープライズ向けのSaaS開発において、SAMLやOIDCといった外部のシングルサインオン(SSO)連携と、ワークスペースへの「招待リスト(Whitelist/Invite List)」によるアクセス制御を組み合わせる設計は非常に一般的である。 通常、SSO連携経由でサインアップするユーザーは、外部の認証プロバイダ(IdP)側で身元が保証されているため、ローカルの招待リストチェックを「バイパス(迂回)」させる設計がとられる。 しかし、このバイパス判定を「グローバルなシステム設定変数(環境変数等)」に依存して大雑把に行っていると、認証モデルのマルチテナント化や動的拡張(マルチSSOサポートなど)を行った際、評価ロジックに不整合が発生し、正当なユーザーが締め出される、あるいは不正な侵入を許すセキュリティ上の隙(Gap)が生まれる。 本稿では、Onyxで発生したマルチSSO移行時の認可バイパス漏れバグ(onyx-dot-app/onyx#13018)を事例に、アイデンティティの「出所(Provenance:プロベナンス)」の厳密な評価設計について分析する。
2. インシデントの分析:マルチSSO構成で発生した「招待リスト」のすれ違い
2.1. 背景:レガシーな「シングルSSO」モデルの限界
Onyxでは当初、システム全体のグローバル環境変数である AUTH_TYPE が SAML または OIDC に直接指定されている状態のときにのみ、「SSO経由のサインアップであるため、ローカルの招待リスト(Invite List)のチェックをバイパスする」という条件分岐(レガシーバイパス)が動いていた。 しかし、複数の外部認証プロバイダと動的に連携する「マルチSSO(OIDCマルチ等)」に対応するため、認証プロバイダの設定をデータベースのレコードとして保存・管理する新しい構成モデル(BASIC Auth + 動的プロバイダ)が導入された。
2.2. 問題:グローバル状態と動的プロバイダ構成の不整合
この新構成モデルでは、システム全体の環境変数 AUTH_TYPE は基本値である BASIC のままで稼働する。 その結果、ユーザーが外部の正当なSSO(IdP)を経由してログインを完了させた際、ログイン処理の最深部にある verify_email_is_invited 関数が AUTH_TYPE == BASIC であると判断し、「このユーザーはSSO管理ユーザーではない」と誤認してしまった。
これにより、外部のIdPによって完全に認証された正当な企業ユーザーであるにもかかわらず、Onyx側のローカル招待リストに含まれていないという理由でログインが理不尽にブロックされるという、深刻な可用性障害(SSOの不全)が発生した。
2.3. 原因:アイデンティティの「出所(Provenance)」の検証漏れ
このセキュリティギャップの本質は、ユーザー登録の呼び出しチェーンにおいて、「このユーザー登録要求が、どの認証経路(VCS / SAML / OIDC / Google / JWT)からやってきたのか」という IDの出所(Provenance:プロベナンス)の情報が、関数のシグネチャをまたいで正しく引き継がれず、中途半端なグローバル環境変数に依存して判定されていた点にある。
2.4. 安易な「チェックの緩和」が招く新たな脆弱性(抜け道)の罠
可用性障害(正規ユーザーの締め出し)を解決する際、開発者が陥りがちな罠は、トラブルの解消を急ぐあまり「チェックゲートの条件を全体的に緩めてしまう」ことである。 例えば、以下のように「OAuthやSSO連携からのサインアップなら、一律で招待リストのチェックをスルーする」という安易な修正(緩和)を施したとする。
# 脆弱な緩和策の例(アンチパターン)
def verify_email_is_invited(email: str, is_oauth_or_sso: bool) -> bool:
if is_oauth_or_sso:
return True # 外部連携経由なら招待チェックをスルー
return check_invite_list(email)この修正は可用性の問題を解決するが、同時に**「Googleログインを用いた、部外者による不正侵入の抜け道(バックドア)」を自動的に空けてしまう。 Googleログイン(一般のOAuth)は、IdP側で社員のアカウント登録が厳密に管理されているSAMLやOIDCマルチとは異なり、部外者であっても自身のGoogleアカウント(attacker@gmail.com 等)さえあれば認証を通すことができる。 この「一般OAuthによるログイン要求」までSSOと同列に扱って招待チェックをスルーさせてしまえば、招待リストによるセキュリティ境界は完全に崩壊し、部外者がGoogleログインボタンを押すだけでワークスペースにフリーパスで侵入可能になる。 すなわち、「可用性を守るためにセキュリティの網を安易に緩めると、別の経路(Google OAuth)に新たな脆弱性が誕生する」**というジレンマに直面する。
3. 実体験:認証プロトコルの混在環境で起きる「アカウント乗っ取り」の隙
認証の「出所(プロベナンス)」の検証を怠り、単に「メールアドレスが一致しているから」や「グローバル設定がこうだから」という緩い条件でユーザー情報をマージ・バイパスする設計は、容易に深刻なアカウント乗っ取り(Account Takeover)を招く。
- 実体験エピソード: 「BASIC認証(ID/パスワード)」と「Googleログイン(OAuth)」の両方をサポートするSaaSアプリケーションを開発した。 当初は、Googleログイン経由のユーザーはメールアドレス検証がGoogle側で済んでいるため、無条件でアクティブユーザーとして登録していた。 しかし、ある日、悪意ある攻撃者が、標的となる既存ユーザー(例:
victim@example.com)のアドレスを使って、BASIC認証(ID/パスワード)でアカウントの「仮登録(メール未検証状態)」を作成した。 その後、同じメールアドレスを何らかの方法で一時的に取得するか、OAuthの検証フローの不備を突き、同じメールアドレスでGoogleログインを実行してシステムにアクセスした。 システム側は、Googleから送られてきた「認証済みメールアドレス」をそのまま信じ、さらに「すでにDBにvictim@example.comのレコード(仮登録)があるからマージしよう」と判断した。この際、「仮登録はBASIC認証で作られた」「GoogleログインはOAuthで作られた」という “出所(パス)の検証” を一切行わずにデータを統合し、かつパスワードの検証をスキップしてログインをバイパスさせた。 結果として、攻撃者が作成したパスワードでそのまま標的のユーザーアカウントにBASIC認証でログインできるようになり、アカウントが完全に乗っ取られた。 「IDの出所(どの認証プロトコルから来たか)」をライフサイクル全体で厳密に区別しない設計は、多重認証環境における致命的な地雷となる。
4. 解決策と「IDプロベナンス」の厳密な伝達設計
Onyxのチームは、PR #13018 において、グローバル環境変数(AUTH_TYPE)への依存を排除し、アイデンティティの出所を明示的にバケツリレーする設計にリファクタリングした。
4.1. キーワード専用引数 sso_managed による出所の明示
ユーザーを作成または検証する一連のコールチェーンに、明示的なフラグ sso_managed を追加した。
- 安全なデフォルト(Safe Default by Default):
sso_managed: bool = Falseとし、引数が省略された通常パス(BASIC認証や通常のOAuth、JWTプロビジョニングなど)では、安全側に倒して「招待リストのチェックを厳密に実行(Fail-Closed)」する。 - 信頼されたコールバックからの明示的指定: OIDCマルチ(
oidc_multi.py)やSAML(saml.py)などの「外部IdPによる厳密な管理が保証されている信頼されたコールバックハンドラー」でのみ、明示的にsso_managed=Trueを引数に渡して、招待リストのバイパスを許可する。
# 安全なデフォルト定義(users.py)
def verify_email_is_invited(
email: str,
*,
sso_managed: bool = False # キーワード専用引数で安全なデフォルトを定義
) -> bool:
if sso_managed:
return True # SSO管理ユーザーは招待リストのチェックをスキップ (Bypass)
# 通常ユーザーは厳密にチェックを執行
return check_invite_list(email)5. 結論
システムの認証やアクセス制御において、「大雑把なグローバル状態(環境変数)」を頼りに認可バイパスを判断することは、システムスケールやマルチテナント化に伴って必ず破綻する。 セキュリティ設計者は、ユーザー登録や権限評価のすべてのデータパスにおいて、「そのIDがどこから検証されてやってきたのか(ID Provenance)」を明示的なパラメータとして引き回し、省略時は最も安全なポリシー(Fail-Closed)が適用される「安全なデフォルト(Secure Default)」 をコードレベルで徹底的に設計しなければならない。
6. 参考文献 / 一次情報
- key the invite-list bypass on per-user SSO provenance: onyx-dot-app/onyx#13018
- Greptile analysis on Onyx user registration security: onyx-dot-app/onyx#13018#issuecomment-4976704379