flawopen.com/Swift の XSS/loadHTMLString
iOS の WKWebView.loadHTMLString は未信頼コンテンツに安全ですか?
自動的に安全になるわけではありません。WKWebView は渡された文字列を HTML として解析するため、信頼できる内容であるか、適切な文脈向けに処理されない限り、タグやスクリプトを実行できるマークアップは有効です。通常のテキストとユーザー作成のリッチ HTML には異なる対策が必要です。
安全な使い方
let html = "\(untrustedText)
" webView.loadHTMLString(html, baseURL: nil)
func escapeHTMLText(_ value: String) -> String {
value
.replacingOccurrences(of: "&", with: "&")
.replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">")
.replacingOccurrences(of: "\"", with: """)
.replacingOccurrences(of: "'", with: "'")
}
let html = "\(escapeHTMLText(untrustedText))
"
webView.loadHTMLString(html, baseURL: nil)
追加で確認するリスク
ページのワールドに登録したメッセージハンドラーは、ページスクリプトからアプリコードへの経路になります。`WKContentWorld` は JavaScript 環境を分離できますが、ページとアプリのスクリプトは DOM を共有します。未信頼 HTML とブリッジを併用するなら、必要最小限の機能だけを公開し、メッセージと frame を検証して各操作を認可してください。ワールド分離を認可と混同しないでください。
baseURL が制御するもの
Apple は `baseURL` を、渡された HTML 内の相対 URL を解決するための基準として説明しています。マークアップを無害化したり、ページの JavaScript を無効にしたりはしません。`nil` は XSS 対策やサンドボックス設定ではありません。リソースとナビゲーションのポリシーは別に決めてください。
境界ごとに対策を選ぶ
HTML のテキストノードに通常の文字列を入れる場合は、以下のようなその文脈専用のエンコーダーを使い、属性、URL、CSS、JavaScript には流用しないでください。ユーザー入力のリッチ HTML には、要素、属性、URL スキームを明示的に許可する保守されたサニタイザーを使います。アプリ側 JavaScript には `callAsyncJavaScript` の引数で値を渡し、ソース文字列に補間しません。`WKContentWorld` は JavaScript 環境を分離しますが DOM は共有するため、ネイティブ操作の認可にはなりません。ブリッジは最小限にし、各メッセージと操作を検証してください。ページ JavaScript が不要なら `allowsContentJavaScript` で追加防御として無効化できますが、適切な出力処理の代わりにはなりません。
よくある質問
いいえ。同じリスクがあり、追加のサニタイズ機能もありません。WKWebView への移行は技術の近代化であり、それだけで安全になるわけではありません。
いいえ。Apple は `baseURL` を相対 URL の解決用として説明しています。HTML サニタイザー、スクリプト切替、サンドボックス保証ではありません。
いいえ。JavaScript 環境は分離しますが、DOM は共有されます。DOM のデータやメッセージは信頼せず、各ネイティブ操作を個別に認可してください。
参考資料
- Apple Developer — WKWebView
- flawopen.com — Swift の XSS
- Apple — WKWebView.callAsyncJavaScript(_:arguments:in:in:completionHandler:)
- Apple — WKContentWorld
- Apple — WKUserContentController.add(_:contentWorld:name:)
- Apple — WKWebpagePreferences.allowsContentJavaScript
- OWASP — クロスサイトスクリプティング対策
- Apple — WKWebView.loadHTMLString(_:baseURL:)