OWASP MASTG に沿って AI エージェントで iOS アプリを自主点検する
外部診断の前段として、脱獄なし・ソースありで iOS アプリを点検する。判定基準は AI に任せず MASTG 各テストの Evaluation 節に固定し、「機械抽出 → 分野別の並列点検 → 1 件ずつ検証」の 3 段に分け、怪しい指摘はシミュレータでダミー値を使って再現まで確かめる。
いつ使うか
- 自社 iOS アプリを外部のセキュリティ診断に出す前に、「どこが怪しいか」を根拠付きで洗い出したいとき。
- AI に「セキュリティレビューして」と丸投げすると、何を確かめたのか曖昧になり、見落としと誤検知が混ざるとき。
- Web 側の AI で自社サービスをセルフペネトレーションする のモバイル版として。
OWASP MAS の 3 文書
| 文書 | 役割 |
|---|---|
| MASVS(v2.1.0) | 何を守るか。8 分野・24 項目 |
| MASWE | モバイルの弱点カタログ(Web の CWE 相当) |
| MASTG(v2.0.0、2026-06 に初の正式版) | どう確かめるか。各テストが Overview / Steps / Observation / Evaluation で構成 |
元記事の集計では、MASTG v2.0.0 の現役 iOS テスト 88 件のうち、静的手順(ソースや設定を読む)を含むのが 64 件。
手順
段階 0: 前提を先に決める
- 機密データの定義(認証情報・個人情報・決済・健康情報・識別子など)。MASTG もテスト前に決めるよう求めている。これがないと「漏洩」を判定できない。
- 乱数/ハッシュ/エンコードと暗号化の区別など、セキュリティ文脈の用語。
- 自社ドメイン一覧(証明書ピンニングの対象を絞る)。
段階 1: 機械的に拾う(AI を使わない)
bash
/usr/libexec/PlistBuddy -c 'Print :NSAppTransportSecurity' Info.plist # ATS の例外
git grep -n 'http://' -- Sources/ # 平文 http
git grep -nE 'serverTrust|URLAuthenticationChallenge|DisabledTrustEvaluator' # 証明書検証の無効化あわせて Dependabot などの依存脆弱性通知が有効かを最初に確認する。GitHub Advisory Database は Swift ライブラリの登録が少なく、「該当なし」は「安全」ではなく「登録がない」と書く。
段階 2: MASVS 分野ごとにサブエージェントで並列点検
- 保存・暗号・通信・WebView・認証・プライバシーなどで担当を分ける。
- 指示は 2 つ: 「
ファイル:行と Evaluation のどの条件に当たるかを書けない指摘は出さない」「合格と判定した項目も、確かめた方法を返す」。
段階 3: 1 件ずつ検証する
- 不合格の指摘は全件、別のエージェントでコードを開いて成立するか確かめる(信頼度スコアの足切りではなく個別検証)。
- 見落とし対策として、高リスク分野の「合格」も再確認させる。
- 重大度は AI に付けさせず、人が「影響度 × 悪用のしやすさ」で付ける。
シミュレータで「本当に起きるか」を確かめる
自分のアプリ・自分の端末・自作のダミー値だけで完結させる。
例 1: カスタム URL スキームで受けた値をそのまま認証情報として保存している
bash
URL=$(python3 -c "import urllib.parse,json; j=json.dumps({'uid':'DUMMY','auth_token':'DUMMY'}); print('myapp://auth?session='+urllib.parse.quote(j))")
xcrun simctl openurl <UDID> "$URL"
CONT=$(xcrun simctl get_app_container <UDID> <bundle-id> data)
grep -ra 'DUMMY' "$CONT/Library/Caches" | head # 注入値がリクエストに乗った痕跡元記事では、外部からスキームを開くと iOS が確認ダイアログを出し、「1 タップ」が必要だった。こうした緩和要因は動かして初めて分かり、深刻度を公平に書く材料になる(開く経路ごとに挙動が変わりうる)。
例 2: WKUserScript でトークンを localStorage に注入している
WKUserScriptはその WebView のメインフレームで読むすべてのページで実行されるので、同じ WebView で外部サイトを開くとそのサイトの localStorage にもトークンが入る。- 確認:
find "$CONT/Library/WebKit" -name 'localstorage.sqlite3'の各 DB をsqlite3 ... "SELECT key FROM ItemTable;"で見て、外部 origin にauthがあれば注入が効いている。 - 直し方:
didCommitでホストを完全一致確認してからevaluateJavaScriptする/外部ページは注入しない別 WebView(.nonPersistent())かSFSafariViewControllerで開く。WKContentWorldは JS の変数空間を分けるだけで localStorage は分けないので効かない。
注意点
- ソースで代替できないテストがある: PIE・スタック保護・ARC(Mach-O を見る)や実効 entitlements(署名済み成果物を見る)は、ソースや設定では「設定上はこう」までしか言えない。合否は「判定できず(成果物が必要)」にする。
- 「合格」は「安全」ではなく「この方法では見つからなかった」。動的テスト・サーバー側・外部診断は別途必要。
- 修正提案では、既存の画面・導線が壊れないか(例: 注入タイミングを
.atDocumentEnd→didCommitにずらして Web 側の読み取りに間に合うか)まで逆向きに確認する。
出典
- @yousukezan · 2026-10-15
- 元記事(Qiita, yasu-ya): qiita.com