テック技報最新号へ →
セキュリティ

OWASP MASTG に沿って AI エージェントで iOS アプリを自主点検する

外部診断の前段として、脱獄なし・ソースありで iOS アプリを点検する。判定基準は AI に任せず MASTG 各テストの Evaluation 節に固定し、「機械抽出 → 分野別の並列点検 → 1 件ずつ検証」の 3 段に分け、怪しい指摘はシミュレータでダミー値を使って再現まで確かめる。

@yousukezan · 2026-10-16
#ios#mobile#owasp#masvs#mastg#ai-agent#code-review#webview

いつ使うか

  • 自社 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 側の読み取りに間に合うか)まで逆向きに確認する。

出典

X の元の投稿を開く