実はiOSのWebView内のHTTPSリクエストは傍受できる(URLProtocol)

Chromeの開発ツールにはNetworkという項目があり，ブラウザでサイトにアクセスしたときのHTTP/HTTPSのアクセスのログを見ることができます．APIの動作確認などできるので，Web開発者なら重宝している機能の一つだと思います．
とても便利な機能なので，iOSのWKWebViewでも使えたらいいなと思い開発しました．
下記の動画が作ったサンプルなのですが，アプリ内のWKWebViewでGithubにアクセスしたときのHTTPSリクエストすべてをTableViewに表示できるようになっています．
こちらが今回作成したサンプルのレポジトリです．
https://github.com/tommy19970714/WebKitURLProtocol
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56557/496dcd5d-06ed-b7e6-fc4d-d6d3695063b9.gif" width="320px">

これはログを表示するだけですが，この技術を応用する例としては，WKWebView上で開いているyahooのホームページ内にある画像をすべて猫の画像にすり替えるChrome Extensionのようなこともできます．

他に事例がないか調べたのですが，通信デバックライブラリの[netfox](https://github.com/kasketis/netfox)や[Wormholy](https://github.com/pmusolino/Wormholy)は非推奨になったUIWebViewのパケットキャプチャはできるようです．WKWebViewに対してパケットキャプチャしている事例はどこにもなかったので今回書くことにしました．

本記事ではURLProtocolについて解説します．ただURLProtocolを普通に使うだけでは，すべてのHTTPSリクエストを見ることはできません．ちょっとした裏技を使ったらできるようになるので，それについて解説します．

よくよく考えたら，SNSやニュースアプリなどアプリ内でブラウザを開くことのできるアプリは多くありますが，ユーザがそのブラウザを使ってアクセスしたログ(SNSのログイン情報や決済情報も含めて)をユーザの許可なく保存できてしまいます．なので悪用はしないでください．

# URLProtocolとは

iOSではインターネット通信を行う際に`URL Loading System`というものを使っています．
HTTP/HTTPSなど通信プロトコルを使ってURLで特定されるWebサイトや画像等のリソースに非同期でアクセスするための仕組みのことです．

URL Loding Systemについて詳しく知りたい方は公式のドキュメントがあります．
https://developer.apple.com/documentation/foundation/url_loading_system

URLProtocolとは`URL Loading System`の通信に使われるインターフェイスで，ネットワーク通信を開始しリクエストを送ってレスポンスを受け取るプロトコルです．
基本的にURLProtocolはデバック時のAPI通信のモックに使用できます．サーバのAPIが出来上がっていないけど，クライアントのiOS側も先に作りたいという場合に，URLProtocolを使うと実際の通信をインターセプトして戻り値を自由に設定できます．

今回はHTTPSすべてのリクエストをインターセプトできるように，カスタマイズしたURLProtocolを作っていきます．

# カスタマイズしたURLProtocolを作る

URLProtocolはサブクラス化することで実装できます．URLProtocolのサブクラスにするには下記の5つのoverrideメソッドを組み込む必要があります．
組み込むにあたってそれぞれのメソッドの簡単な説明を示します．

```
class CustomURLProtocol: URLProtocol {
    override class func canInit(with request: URLRequest) -> Bool
    // このURLProtocolが引数に渡されるrequestを扱う必要があるかを判断します．trueにするとこのURLProtocolでインターセプトできます．
    
    override open class func canInit(with task: URLSessionTask) -> Bool
    // このURLProtocolが引数に渡されるtaskを扱う必要があるかを判断します．trueにするとこのURLProtocolでインターセプトできます．
    
    override func startLoading()
    // リクエストのロードが開始したときに呼び出されます．
    
    override func stopLoading()
    // リクエストの読み込みが終了するときに呼び出されます．
    
    open override class func canonicalRequest(for request: URLRequest) -> URLRequest
    // リクエストで送られたURLRequestにヘッダーをカスタマイズできます．特に何もする必要がなければ引数のrequestをそのまま返します．
}
```

最初に見せたサンプルのようにアクセスした先のURLを知るには，`canInit`メソッドの`URLRequest`か`URLSessionTask`内を見れば良いです．サンプルでは`canInit`が呼ばれたときに`URLSessionTask`の中身をtableviewに表示しています．

そしてカスタマイズしたURLProtocolは次のようにして，登録できます．

```
URLProtocol.registerClass(CustomURLProtocol.self)
```

CustomURLProtocolを登録をしたら．WKWebViewでページが移動する度に，それぞれのoverrideメソッドが呼ばれるようになります．
ただ今の段階だと，ページ移動時したときのURLしか分からないので，これからすべてのHTTPSリクエストに反応できるように設定していきます．

# URLProtocolのschemeを設定する

URLProtocol関連で調べていたら，たまたま次のレポジトリを発見しました．
https://github.com/Yeatse/NSURLProtocol-WebKitSupport
そのレポジトリではObjective-cでURLProtocolのAppleのドキュメントに載っていない非公開APIについて記述してありました．そのAPIはURLProtocolのトリガーとしてのschemeを設定できるようです．つまり，schemeとして「https」を設定したら，httpsから始まるリクエストにアクセスする度にURLProtocolを呼び出すというトリガーを設定できるということです．

それをswiftに書き換えたのが下記のコードです．

```
extension URLProtocol {
    
    class func contextControllerClass()->AnyClass {
        return NSClassFromString("WKBrowsingContextController")!
    }
    
    class func registerSchemeSelector()->Selector {
        return NSSelectorFromString("registerSchemeForCustomProtocol:")
    }
    
    class func unregisterSchemeSelector()->Selector {
        return NSSelectorFromString("unregisterSchemeForCustomProtocol:")
    }
    
    class func wk_register(scheme:String){
        let cls:AnyClass = contextControllerClass()
        let sel = registerSchemeSelector()
        if cls.responds(to: sel) {
            _ = (cls as AnyObject).perform(sel, with: scheme)
        }
    }
    
    class func wk_unregister(scheme:String){
        let cls:AnyClass = contextControllerClass()
        let sel = unregisterSchemeSelector()
        if cls.responds(to: sel) {
            _ = (cls as AnyObject).perform(sel, with: scheme)
        }
    }
}
```

上記のURLProtocol Extensionを使用すると，次のようにURLProtocolのトリガーのSchemeを設定できます．

```
URLProtocol.wk_register(scheme: "https")
URLProtocol.wk_register(scheme: "http")
```

上記のコードを実行することで，https/httpのすべてのリクエストが呼ばれた際に先ほど登録したカスタムURLProtocolが呼ばれるようになります．


# まとめ

この記事では，次の二段階ですべてのHTTP/HTTPSリクエストをパケットキャプチャできるようにしました．
1. カスタマイズしたURLProtocolを作る
2. URLProtocolのschemeを設定する
今回はたまたま発見した非公開APIを使用したやり方になっているため，推奨できません．あくまで開発のデバックの一つとして使用してください．
審査に通るかどうかは別にして，パケットキャプチャして得たURLを元にWebView上で見ている動画をダウンロードするツールとかは，この技術を使用して作れるかもしれないですね．
もしこの記事を見たAppleのレビュワーさんがいたら，この非公開APIの審査を厳しくしたほうが良さそうです．

# 追記

海外のセキュリティの専門家がこの脆弱性(非公開API)を利用して，脱獄せずに他のアプリのHTTP/HTTPSリクエストを傍受する方法を指摘している記事を見つけました．
どうやって他のアプリを傍受するかというと，今回のコードをライブラリとして書き出してしまって，端末内のアプリのディレクトリに直接入れるだけとのことです．なぜそんなことができてしまうかというと，アプリ内で一度URLProtocolを登録してしまうと，必ずURLProtocolが呼ばれてしまうというstaticな設計になっているためです．これは設計を変えたほうが良さそうです．書きながら思ったのですが，もしこれをmacOSで同じことができてしまうのなら，結構大きなセキュリティホールになるのではと思ってしまいました．今度，macOSでも他のアプリの通信内容を傍受できてしまうのかを実験してみたいと思います．

[medium - Let’s write Swift code to intercept SSL Pinning HTTPS Requests](
https://medium.com/@kennethpoon/lets-write-swift-code-to-intercept-ssl-pinning-https-requests-12446303cc9d)
[Network Interception - Write Swift codes to inspect network requests (even with SSL Pinning active)](https://www.slideshare.net/depoon/network-interception-write-swift-codes-to-inspect-network-requests-even-with-ssl-pinning-active)

# 参考文献
[netfox](https://github.com/kasketis/netfox): URLProtocolを実装する際に参考にしました
[【Swift】URLProtocolという名のclassについて](https://qiita.com/shiz/items/82b24b1e2f2819da4650): URLProtocolについて説明する際に参考にしました．