From ff84547670f8049db206d9b3f16281b774903483 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Tue, 21 Jul 2026 11:44:58 +0530 Subject: [PATCH 1/5] restores default TLS validation in survey webview --- .../FormbricksSDK/WebView/SurveyWebView.swift | 9 ++-- .../FormbricksSDKTests.swift | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/Sources/FormbricksSDK/WebView/SurveyWebView.swift b/Sources/FormbricksSDK/WebView/SurveyWebView.swift index 671ccdd..d539ab5 100644 --- a/Sources/FormbricksSDK/WebView/SurveyWebView.swift +++ b/Sources/FormbricksSDK/WebView/SurveyWebView.swift @@ -92,11 +92,10 @@ extension SurveyWebView { } func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { - if let serverTrust = challenge.protectionSpace.serverTrust { - completionHandler(.useCredential, URLCredential(trust: serverTrust)) - } else { - completionHandler(.useCredential, nil) - } + // Let the OS perform standard certificate-chain validation. Never force-trust + // arbitrary certificates, as that would disable TLS validation and expose the + // survey WebView traffic to man-in-the-middle interception. + completionHandler(.performDefaultHandling, nil) } } } diff --git a/Tests/FormbricksSDKTests/FormbricksSDKTests.swift b/Tests/FormbricksSDKTests/FormbricksSDKTests.swift index 83b9013..253d6f3 100644 --- a/Tests/FormbricksSDKTests/FormbricksSDKTests.swift +++ b/Tests/FormbricksSDKTests/FormbricksSDKTests.swift @@ -1,4 +1,5 @@ import XCTest +import WebKit @testable import FormbricksSDK final class FormbricksSDKTests: XCTestCase { @@ -751,4 +752,52 @@ final class FormbricksSDKTests: XCTestCase { wait(for: [legacyExpectation, newExpectation], timeout: 2.0) } + + /// Security regression guard: the survey WebView must delegate + /// TLS certificate validation to the OS and must never force-trust the + /// server certificate. Force-trusting any certificate disables chain + /// validation and exposes survey traffic to man-in-the-middle interception. + func testWebViewAuthChallengeUsesDefaultHandlingAndDoesNotForceTrust() { + let coordinator = SurveyWebView.Coordinator() + let webView = WKWebView() + let protectionSpace = URLProtectionSpace( + host: "example.com", + port: 443, + protocol: NSURLProtectionSpaceHTTPS, + realm: nil, + authenticationMethod: NSURLAuthenticationMethodServerTrust + ) + let challenge = URLAuthenticationChallenge( + protectionSpace: protectionSpace, + proposedCredential: nil, + previousFailureCount: 0, + failureResponse: nil, + error: nil, + sender: NoopChallengeSender() + ) + + var capturedDisposition: URLSession.AuthChallengeDisposition? + var capturedCredential: URLCredential? + let handlerCalled = expectation(description: "completion handler called") + coordinator.webView(webView, didReceive: challenge) { disposition, credential in + capturedDisposition = disposition + capturedCredential = credential + handlerCalled.fulfill() + } + wait(for: [handlerCalled], timeout: 1.0) + + XCTAssertEqual(capturedDisposition, .performDefaultHandling, + "WebView must let the OS validate the certificate chain, not override it.") + XCTAssertNil(capturedCredential, + "WebView must not supply a credential that force-trusts the server certificate (MITM risk).") + } +} + +/// Minimal sender so a `URLAuthenticationChallenge` can be constructed in tests. +/// The handler under test only inspects the disposition it hands back, so these +/// callbacks intentionally do nothing. +private final class NoopChallengeSender: NSObject, URLAuthenticationChallengeSender { + func use(_ credential: URLCredential, for challenge: URLAuthenticationChallenge) {} + func continueWithoutCredential(for challenge: URLAuthenticationChallenge) {} + func cancel(_ challenge: URLAuthenticationChallenge) {} } From 4a7cfeed232e970d36db79b6b0240d996b502ff5 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Tue, 21 Jul 2026 12:00:18 +0530 Subject: [PATCH 2/5] fix: harden survey webview (ENG-1811, ENG-1812, ENG-1813) - ENG-1811: gate WKWebView.isInspectable behind #if DEBUG so the survey WebView is not inspectable in release builds. - ENG-1812: restrict external URLs opened from survey content to http/https; refuse tel/sms/custom-app/file/javascript schemes. - ENG-1813: base64-encode the survey payload before embedding it in the WebView HTML instead of splicing it into a JS template literal, removing the script-injection surface. Also drops the quote-mangling workaround that corrupted survey text containing double quotes. Adds regression tests for ENG-1812 and ENG-1813 and updates the existing WEBVIEW_DATA test to decode the base64 payload. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../WebView/FormbricksViewModel.swift | 16 +++- .../FormbricksSDK/WebView/SurveyWebView.swift | 26 ++++++- .../FormbricksSDKTests.swift | 75 +++++++++++++++++-- 3 files changed, 103 insertions(+), 14 deletions(-) diff --git a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift index 9500c14..3728a2b 100644 --- a/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift +++ b/Sources/FormbricksSDK/WebView/FormbricksViewModel.swift @@ -10,7 +10,12 @@ final class FormbricksViewModel: ObservableObject { self.surveyId = surveyId if let webviewDataJson = WebViewData(workspaceResponse: workspaceResponse, surveyId: surveyId).getJsonString(), let surveyScriptUrl = FormbricksWorkspace.surveyScriptUrlString { - htmlString = htmlTemplate.replacingOccurrences(of: "{{WEBVIEW_DATA}}", with: webviewDataJson) + // Base64-encode the payload before injecting it into the HTML. Base64 output is + // limited to [A-Za-z0-9+/=], so survey content can no longer contain characters + // (backticks, `${...}`, quotes) that would break out of the surrounding JS string + // literal and execute as code. The WebView decodes it back to JSON at runtime. + let webviewDataBase64 = Data(webviewDataJson.utf8).base64EncodedString() + htmlString = htmlTemplate.replacingOccurrences(of: "{{WEBVIEW_DATA}}", with: webviewDataBase64) .replacingOccurrences(of: "{{SURVEY_SCRIPT_URL}}", with: surveyScriptUrl) } } @@ -33,7 +38,9 @@ private extension FormbricksViewModel {