本レポートは、Safe Exam Browser (SEB) がWebページに注入(インジェクション)するJavaScriptの役割と、ネイティブコードとの連携メカニズムを解明することを目的とする。特に、以前の議論で仮説として挙げた「JavaScriptを介したチャレンジ・レスポンス」のような高度な検証機能が存在するのかどうか、コード上の証拠に基づいて明らかにすることに主眼を置く。
SEBのJavaScript連携は、WebKitのWKUserContentControllerとWKScriptMessageHandlerプロトコルを用いて実現されている。主な処理はClasses/BrowserComponents/SEBAbstractModernWebView.swiftに集約されている。
プロセスは以下の通り。
- JavaScriptコードの注入: WebViewが初期化される際、
WKUserContentControllerを使用して、SEBが用意したカスタムJavaScriptコードをすべてのWebページに注入する。 - グローバルAPIの提供: 注入されたJavaScriptは、
window.SafeExamBrowserというグローバルオブジェクトを作成し、Webページ側から呼び出し可能なAPIを定義する。 - メッセージハンドラの登録: ネイティブ側(Swift)は、
WKScriptMessageHandlerプロトコルに準拠し、JavaScriptからのメッセージを受け取るためのハンドラを登録する。 - 双方向通信:
- Webページは
window.SafeExamBrowserのAPIを呼び出す。 - APIは
window.webkit.messageHandlers.[ハンドラ名].postMessage()を使ってネイティブ側にメッセージを送信する。 - ネイティブ側は
userContentController:didReceiveScriptMessage:メソッドでメッセージを受信し、対応する処理を実行する。 - ネイティブ側は
evaluateJavaScript()を使って、結果を返すためのJavaScriptコードを実行する。
- Webページは
SEBAbstractModernWebView.swiftのwkWebViewConfigurationプロパティ内で、注入されるJavaScriptコードが定義されている。
// Classes/BrowserComponents/SEBAbstractModernWebView.swift
let jsApiCode = """
window.SafeExamBrowser = {
version: '\(appVersion ?? "")',
security: {
browserExamKey: '',
configKey: '',
appVersion: '\(appVersion ?? "")',
updateKeys: function (callback) {
if (callback) {
window.webkit.messageHandlers.updateKeys.postMessage(callback.name);
} else {
window.webkit.messageHandlers.updateKeys.postMessage();
}
}
}
}
"""
let jsApiUserScript = WKUserScript(source: jsApiCode, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(jsApiUserScript)このコードにより、Webページ上のJavaScriptは window.SafeExamBrowser オブジェクトにアクセスできるようになる。このオブジェクトが提供する機能は以下の通り。
SafeExamBrowser.version: SEBのバージョン文字列を返す。SafeExamBrowser.security.appVersion: SEBのバージョン文字列を返す。(重複している)SafeExamBrowser.security.browserExamKey: (初期値は空文字列)SafeExamBrowser.security.configKey: (初期値は空文字列)SafeExamBrowser.security.updateKeys(callback): ネイティブ側にupdateKeysというメッセージを送信する関数。ネイティブ側での処理完了後にcallback関数を実行させることができる。
JavaScriptから updateKeys メッセージが送信されると、ネイティブ側の userContentController(_:didReceive:) メソッドが呼び出される。
// Classes/BrowserComponents/SEBAbstractModernWebView.swift
public func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
if message.name == "updateKeys" {
let frame = message.frameInfo
// ...
updateKeyJSVariables(sebWebView, frame: frame) { response, error in
// ... コールバック処理 ...
}
}
// ...
}updateKeys メッセージを受け取ると、-updateKeyJSVariables メソッドが実行される。このメソッドの役割が、今回の調査の核心である。
// Classes/BrowserComponents/SEBAbstractModernWebView.swift
private func updateKeyJSVariables(_ webView: WKWebView, frame: WKFrameInfo?, completionHandler: ((Any?, Error?) -> Void)? = nil) {
// ... urlを取得 ...
if url != nil {
let browserExamKey = navigationDelegate?.browserExamKey?(for: url!)
let configKey = navigationDelegate?.configKey?(for: url!)
// ...
webView.evaluateJavaScript("SafeExamBrowser.security.browserExamKey = '\(browserExamKey ?? "")';SafeExamBrowser.security.configKey = '\(configKey ?? "")';") { (response, error) in
completionHandler?(response ?? "", error)
}
}
}このコードが示しているのは、以下の事実である。
- JavaScriptから
updateKeysが要求されると、ネイティブ側はその時点でのURLに対して**browserExamKeyとconfigKeyを計算**する。 - そして、計算したハッシュ値を
evaluateJavaScriptを使って、window.SafeExamBrowser.security.browserExamKeyとwindow.SafeExamBrowser.security.configKeyの値を上書きする。
私の仮説は、半分正しく、半分間違っていました。
- 正しかった点: SEBは、Webページ上のJavaScriptと連携し、動的にハッシュキーを操作する仕組みを持っている。
- 間違っていた点: その仕組みは、私が想定した「サーバーからのチャレンジコードに署名する」というチャレンジ・レスポンスモデルではなかった。
SEBが実装しているのは、よりシンプルな**「オンデマンドでのキー計算と提供」**モデルである。
- LMSのWebページは、読み込まれた時点ではハッシュ値を知らない。
- ページ内のJavaScriptが、任意のタイミングで
SafeExamBrowser.security.updateKeys()を呼び出す。 - SEBクライアントは、そのリクエスト(のURL)に対応する正しい
browserExamKeyとconfigKeyを計算し、JavaScriptのグローバル変数に設定する。 - その後、JavaScriptはこれらの(計算済みで信頼できる)キーの値を読み取り、例えばフォームの隠しフィールドに設定したり、Ajaxリクエストでサーバーに送信したりすることができる。
このモデルは、サーバー側がクライアントのソルトを知らなくても、クライアントの正当性を確認する巧妙な方法を提供している。
-
LMSサーバーの動作:
.seb設定を配布する際、サーバーはソルトを含めずに配布する。- しかし、サーバーは**「この設定ファイルから計算されるべき
configKey(ソルト不要のハッシュ)」の値は知っている。** - サーバーは、この期待される
configKeyの値を、ユーザーセッションに紐付けて保存しておく。
-
クライアント(SEB)の動作:
- SEBは
.sebファイルを読み込み、ランダムなソルトを生成してbrowserExamKeyを計算する。同時に、ソルト不要のconfigKeyも計算する。
- SEBは
-
検証プロセス:
- LMSのJavaScriptが
updateKeys()を呼び出し、configKeyを取得する。 - JavaScriptが、取得した
configKeyをサーバーに送信する。 - サーバーは、送られてきた
configKeyと、セッションに保存しておいた期待値が一致するかどうかを比較する。
- LMSのJavaScriptが
この方法により、サーバーはクライアントが「正しい設定ファイルを使用している」ことを、ソルトを知ることなく検証できる。 browserExamKey は、主に RequestHash ヘッダの生成に使われ、直接の検証対象ではない可能性が高い。
最終的に、SEBの偽装は、configKey の計算ロジック(JSON化のルールなど)を完璧に再現し、かつこのJavaScript連携に応答できない限り、やはり困難であるという結論になる。