MiniRent 工程指南

在雲端 Mac 稽核 iOS ATS 例外並建立 TLS 門禁

在雲端 Mac 稽核 iOS ATS 例外並建立 TLS 門禁

測試環境原本一直能正常存取 API,但將封存套件交給測試團隊後,卻立刻出現網路錯誤。另一種更危險的情況則正好相反:為了暫時繞過憑證問題,有人將 NSAllowsArbitraryLoads 留在發佈設定中。應用程式看似恢復正常,實際上卻放行了所有原本應由 ATS 阻擋的連線。要避免這兩類事故,不能只檢查版本庫中的 Info.plist,而應以雲端 Mac 實際建置出的 App 為準。

先定義門禁要檢查什麼

一套可執行的 ATS 門禁至少分為三層:靜態設定、主機端 TLS 交握,以及應用層請求。三層回答的問題各不相同,不能用一次成功的 curl 取代所有驗證。

層級 檢查對象 失敗時優先檢查
靜態設定 App 內最終的 Info.plist 建置設定、指令碼改寫、網域例外
TLS 探測 雲端 Mac 到目標端點 DNS、憑證鏈、通訊協定版本、重新導向
應用程式請求 URLSession 實際行為 ATS、工作階段設定、驗證與回應解析

首先明確訂出政策:發佈產物不得包含 NSAllowsArbitraryLoadsNSExceptionDomains 只能列出經過審查的網域;臨時例外必須註明負責人與移除條件;僅供本機除錯使用的設定不得混入 Release。

ATS 例外不是「先讓網路跑起來」的通用開關,而是一項必須說明影響範圍、原因與退出計畫的安全性變更。

稽核最終建置產物

原始碼中的 plist 可能會被 INFOPLIST_KEY_*、不同的 xcconfig 或建置指令碼覆寫。先完成一次 Release 建置,再取得產物路徑:

set -euo pipefail

xcodebuild \
  -scheme "$SCHEME" \
  -configuration Release \
  -sdk iphonesimulator \
  -derivedDataPath "$PWD/.derived-data" \
  build

APP_PATH="$(find "$PWD/.derived-data/Build/Products" \
  -type d -name '*.app' -path '*Release-*' -print -quit)"

test -n "$APP_PATH"
PLIST="$APP_PATH/Info.plist"
plutil -lint "$PLIST"
plutil -extract NSAppTransportSecurity json -o - "$PLIST" \
  > "$PWD/ats-effective.json" 2>/dev/null || printf '{}
' > "$PWD/ats-effective.json"

門禁應將「沒有 ATS 字典」與「存在空字典」視為正常情況,而不是判定為錯誤。真正應該失敗的是過度寬鬆的放行設定或未經批准的例外。以下指令碼透過環境變數傳入允許清單,避免將團隊內部網域硬編碼到公開指令碼中:

import json
import os
import sys

with open(sys.argv[1], encoding="utf-8") as f:
    ats = json.load(f)

if ats.get("NSAllowsArbitraryLoads") is True:
    raise SystemExit("NSAllowsArbitraryLoads is forbidden")

approved = {
    item.strip().lower()
    for item in os.getenv("ATS_APPROVED_DOMAINS", "").split(",")
    if item.strip()
}
exceptions = ats.get("NSExceptionDomains", {})

unknown = sorted(set(map(str.lower, exceptions)) - approved)
if unknown:
    raise SystemExit("Unapproved ATS domains: " + ", ".join(unknown))

執行時使用 python3 ci/audit_ats.py ats-effective.json。CI 記錄可以保留鍵名與檢查結果,但不要輸出請求權杖、Cookie 或完整的驗證標頭。

為例外建立可審查的清單

只比較網域仍然不夠。每個例外都應記錄允許使用的 ATS 鍵、適用環境、原因與重新審查條件。尤其要注意 NSIncludesSubdomains:它會將影響範圍擴大至所有子網域,不能因為目前只有一個 API 就預設開啟。

建議將清單以 JSON 或 YAML 格式維護在版本庫中,並在審查時核對下列項目:

  • 網域必須精確,不接受萬用字元式描述;
  • 禁止降級至不符合專案基準的 TLS 版本;
  • 禁止為了處理單一重新導向而放行整個父網域;
  • 除錯用 API 只能加入 Debug 設定;
  • 移除例外後,必須重新執行建置與請求測試。

檢查設定是否混入其他組態

分別建置 Debug 與 Release,接著匯出兩份 ats-effective.json 進行差異比較。如果 Release 中出現只供封包擷取或本機服務使用的鍵,門禁應直接判定失敗。不要只比較原始碼檔案,因為即使使用同一個 plist,不同的建置設定也可能注入不同結果。

探測 TLS 與重新導向鏈

靜態稽核通過後,再從實際負責建置工作的雲端 Mac 探測目標位址。nscurl 可以輸出 ATS 診斷矩陣,適合用來定位通訊協定、憑證鏈與前向保密問題:

test -n "${API_URL:-}"
/usr/bin/nscurl --ats-diagnostics "$API_URL" \
  > "$PWD/ats-diagnostics.txt" 2>&1

這份輸出適合診斷,但不適合單純以「檔案中出現 PASS」作為門禁判定,因為診斷模式會嘗試多種放寬限制的組合。CI 的強制判定應使用專案的實際 URL 發出一次受控請求,並限制逾時時間、重新導向次數與回應狀態碼:

curl --fail --silent --show-error \
  --proto '=https' \
  --tlsv1.2 \
  --max-time 15 \
  --max-redirs 3 \
  --output /dev/null \
  "$API_URL"

curl 成功只代表主機端連線路徑可用。它不會套用 iOS App 的 ATS 設定,也無法證明應用程式的工作階段代理、請求標頭或驗證邏輯正確。

補上應用層迴歸測試

最後新增一個使用正式網路堆疊的輕量測試目標。測試應讀取由測試環境注入的 URL,透過 URLSession 發出健康檢查請求,並斷言請求已完成、狀態碼符合約定,而且沒有重新導向至非 HTTPS 位址。不要在測試程式碼中寫入固定憑證。

保存充分但不過量的證據

失敗時只需封存以下內容:最終 ATS 字典、網域清單比較結果、nscurl 輸出、請求狀態碼、重新導向目標的主機名稱,以及 Xcode 建置設定名稱。憑證全文、存取權杖與完整回應本文通常沒有必要寫入長期記錄。

將執行順序固定為「Release 建置 → 最終 plist 稽核 → TLS 探測 → URLSession 測試」。如此既能發現設定漂移,也能判斷究竟是 App 政策拒絕連線,還是目標端點的憑證鏈、DNS 或重新導向發生變化。

上線前檢查表

合併前確認發佈產物沒有任意網路放行設定,所有網域例外都列在批准清單中,子網域範圍已經過明確審查,TLS 探測使用真實目標位址,而且應用層測試採用實際的 URLSession 設定。發生失敗時,先保留產物與診斷檔案,再修改設定,避免以新增例外掩蓋憑證或重新導向問題。

這套門禁的目的不是讓所有網路故障自動消失,而是將問題定位到可處理的層級,並確保臨時除錯設定不會悄悄進入下一次發佈。

常見問題

為什麼不能只檢查原始碼中的 Info.plist?

建置設定、組態檔與腳本都可能改寫最終值,因此必須檢查 App 產物內真正交付的 Info.plist。

nscurl 診斷通過就代表 App 一定能連線嗎?

不代表。它只確認主機層的 TLS 能力,仍須使用 URLSession 驗證 ATS 規則、重新導向與驗證流程。

哪些 ATS 設定應直接讓 CI 失敗?

應阻擋 NSAllowsArbitraryLoads、未列入核准清單的網域例外,以及誤入發佈產物的除錯專用放行設定。

獨享實體設備

使用雲端 Mac mini 執行下一項開發或建置任務

可從兩種 M4 設定、四種租用週期與五個可租用節點中選擇,實際可用狀態以控制台即時回傳為準。

選擇設定並租用