雲端 Mac 上的 iOS Base64URL 嚴格解碼驗收

雲端 Mac 上的 iOS Base64URL 嚴格解碼驗收

雲端 Mac 上的 iOS Base64URL 嚴格解碼驗收

API 將一段二進位游標編碼為 Base64URL,iOS 用戶端卻偶爾把損壞的游標視為有效資料。問題不一定出在網路:寬鬆解碼、自動補齊填補字元,以及忽略非法字元,都可能將原本應該拒絕的輸入轉成位元組。在雲端 Mac 上,可以先用獨立的 Swift 腳本確立解碼契約,再將同一組測試案例移入 App 的測試目標,不必啟動模擬器就能檢查編碼的邊界條件。

先明確定義接受哪一種編碼

本文只接受非空、不含填補字元的 Base64URL:可用字元為英文字母、數字,以及 -、_,不接受空格、換行、+、/ 或 =。這是應用程式 API 明確選定的規則,不代表所有使用 Base64 的協定都應採用相同規則。

標準 Base64 與 Base64URL 不應在同一個入口靠猜測來相容。若舊 API 要求帶有填補字元,就應提供獨立的解析入口並分別測試,避免用戶端升級後,在不知不覺中擴大可接受的輸入範圍。

長度也有結構上的限制:不含填補字元的文字,其字元數除以四,餘數只能是零、二或三;餘數為一時,不可能構成合法編碼。範例將輸入上限設為 65536 個 UTF-8 位元組,這是測試用的應用程式策略,不是協定規定的數值。

解碼只會還原位元組,無法證明內容可信。用於權杖、游標或簽章參數時,仍須驗證其結構、來源及業務有效性。

透過重新編碼確保使用正規表示法

將以下程式碼儲存為 base64url-check.swift。先限制輸入大小,再檢查字元集,最後只在內部補齊填補字元。不要使用忽略未知字元的選項,也不要預先刪除空白字元。

import Foundation

enum DecodeFailure: Error {
    case size
    case alphabet
    case length
    case payload
    case noncanonical
}

func decodeBase64URL(
    _ text: String,
    maxEncodedBytes: Int = 65536
) throws -> Data {
    let count = text.utf8.count
    guard count > 0, count <= maxEncodedBytes else {
        throw DecodeFailure.size
    }

    let valid = text.utf8.allSatisfy { byte in
        (65...90).contains(byte)
        || (97...122).contains(byte)
        || (48...57).contains(byte)
        || byte == 45
        || byte == 95
    }
    guard valid else {
        throw DecodeFailure.alphabet
    }

    let remainder = count % 4
    guard remainder != 1 else {
        throw DecodeFailure.length
    }

    var padded = text
        .replacingOccurrences(of: "-", with: "+")
        .replacingOccurrences(of: "_", with: "/")
    padded += String(repeating: "=", count: (4 - remainder) % 4)

    guard let data = Data(base64Encoded: padded) else {
        throw DecodeFailure.payload
    }

    let canonical = data.base64EncodedString()
        .replacingOccurrences(of: "+", with: "-")
        .replacingOccurrences(of: "/", with: "_")
        .replacingOccurrences(of: "=", with: "")

    guard canonical == text else {
        throw DecodeFailure.noncanonical
    }
    return data
}

最後一次比較不能省略。單一位元組 FF 的正規表示法是 _w;_x 未使用的尾端位元並非零,但某些解碼器仍可能將它還原為相同的位元組。比較重新編碼後的結果,才能讓驗收不受底層解碼器寬鬆程度的影響。

這裡回傳 Data,不直接轉換為字串。合法的二進位資料不一定是 UTF-8;需要文字的上層必須另外檢查編碼,不能用替代字元掩蓋內容損壞的問題。

將失敗案例寫成可執行的檢查

將以下測試加在同一個檔案的末尾。測試失敗時會擲出錯誤,讓腳本以非零結束代碼退出,不依賴可能在最佳化建置中遭到移除的斷言。

enum CheckFailure: Error {
    case mismatch
    case acceptedInvalid
}

let validCases: [(String, [UInt8])] = [
    ("AA", [0]),
    ("_w", [255]),
    ("-_8", [251, 255]),
    ("Zm9v", [102, 111, 111])
]

for (text, expected) in validCases {
    guard try decodeBase64URL(text) == Data(expected) else {
        throw CheckFailure.mismatch
    }
}

let invalidCases = [
    "", "A", "Zg==", "Zg\n", "+w", "/w", "_x", "中文"
]

for text in invalidCases {
    do {
        _ = try decodeBase64URL(text)
    } catch is DecodeFailure {
        continue
    }
    throw CheckFailure.acceptedInvalid
}

do {
    _ = try decodeBase64URL("AAAA", maxEncodedBytes: 3)
} catch is DecodeFailure {
    exit(0)
}
throw CheckFailure.acceptedInvalid

在已設定命令列工具的雲端 Mac 上執行:

xcrun swift base64url-check.swift

成功時不會有任何輸出,結束代碼為零。_x 用來涵蓋尾端位元檢查;含有換行的案例可防止日後有人加入自動清理;中文案例則確認字元集是依 ASCII 位元組驗證,而不是以「看起來像字母」作為判斷依據。

檢查大小邊界與呼叫位置

輸入上限的檢查必須放在字串替換與解碼之前,否則過長的內容早已產生額外副本。這裡允許的最大正規輸入可解碼為 49152 個位元組,但這只是編碼層的資源預算;業務欄位通常應設定更小的上限。

函式收到字串時,字串已經存在於記憶體中,因此這項限制無法取代網路回應的大小限制。接收層需要限制回應本文,解碼層限制欄位,結構解析層再限制集合的元素數量與巢狀深度,由三個層級分別負責。

還需要補齊三個邊界案例:

也要避免一邊將解碼結果作為識別依據,另一條路徑卻接受寬鬆版本。快取鍵、去重鍵與簽章輸入採用哪一種表示法,應在 API 契約中統一,否則相同內容可能得到不同的處理結果。

納入迴歸測試並限制診斷資訊範圍

獨立腳本通過後,將解碼函式放入共用模組,並將測試案例移入 App 的單元測試目標。命令列腳本適合快速重現問題,App 目標的測試則負責找出實際相依性與建置設定帶來的差異,兩者不能長期各自維護一份實作。

除了固定案例,也可以針對不同長度的位元組陣列執行「編碼、解碼、逐位元組比較」。重點是涵蓋長度除以三時各種不同的餘數,因為它們決定最後一組編碼會使用多少尾端位元。接著修改末尾字元,確認非正規表示法無法蒙混過關。

失敗報告只保留錯誤類別、輸入長度與案例編號,不記錄真實權杖或完整游標。上線前確認:沒有自動裁剪、沒有忽略未知字元、沒有混用字元集,而且結構驗證是在解碼之後進行。最終目標不是讓更多輸入成功解碼,而是讓 API 只接受雙方約定的那一種表示法。

常見問題

為什麼解碼後還要重新編碼比對?

部分解碼器會接受未使用尾端位元非零的輸入。將結果重新編碼為正規的無填補 Base64URL 並逐字比對,可以拒絕同一位元組序列的非正規文字表示。

這個解碼器可以驗證權杖簽章嗎?

不可以。它只驗證編碼形式並回傳位元組,不驗證簽章、有效期限或發行者。權杖仍須經過獨立的密碼學驗證與業務規則檢查。

用自己的 CI/CD 流程驗證雲端 Mac

ArmRDP 提供專屬實體 Mac mini,並非虛擬機。可按天租用,先確認工具鏈與建置結果,再選擇適合團隊的租期。

立即租用雲端 Mac

設定有據可查,建置結果可重現

確認機型、租期與連線步驟,再將工作交給專屬實體節點。