Skip to content

fix: harden survey webview — TLS, script injection, URL scheme, inspectable (ENG-1810/1811/1812/1813) - #51

Merged
pandeymangg merged 5 commits into
mainfrom
anshuman/eng-1810-ios-sdk-survey-webview-accepts-any-tls-certificate-mitm
Jul 22, 2026
Merged

fix: harden survey webview — TLS, script injection, URL scheme, inspectable (ENG-1810/1811/1812/1813)#51
pandeymangg merged 5 commits into
mainfrom
anshuman/eng-1810-ios-sdk-survey-webview-accepts-any-tls-certificate-mitm

Conversation

@pandeymangg

@pandeymangg pandeymangg commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Security hardening of the survey WebView, from the SJ app-team report (patched in their iOS fork). Four related fixes, all confined to SurveyWebView.swift and FormbricksViewModel.swift.

Ticket Issue Severity
ENG-1810 WebView accepted any TLS certificate (MITM) Critical
ENG-1813 Survey payload spliced into a JS template literal (script injection) High → Medium
ENG-1812 External URL scheme not validated before opening Medium/Low
ENG-1811 isInspectable enabled in release builds Low

ENG-1810 — Restore default TLS validation

The WKNavigationDelegate auth-challenge handler answered every challenge with URLCredential(trust:), accepting any server certificate and disabling chain validation for the whole survey WebView — trivial man-in-the-middle interception of payload, responses, and config.

Fix: respond with completionHandler(.performDefaultHandling, nil) so the OS validates the certificate chain.

ENG-1813 — Payload no longer injected as executable JS

The survey payload was find-replaced into a backtick template literal (const json = `{{WEBVIEW_DATA}}`). Survey content containing a backtick — or ${...}, which template literals evaluate eagerly — could break out and run arbitrary JS in the WebView.

Fix: base64-encode the payload in Swift and decode it at runtime (atobTextDecoderJSON.parse). Base64 output is [A-Za-z0-9+/=] only, so it cannot contain a breakout character. Also removed a \"' workaround that was corrupting survey text with double quotes.

ENG-1812 — Restrict external URL schemes

Links from survey content were passed straight to UIApplication.shared.open, allowing any registered scheme (tel:, sms:, custom app deep links, etc.).

Fix: allowlist http/https via JsMessageHandler.isAllowedExternalURL(_:); everything else is refused and logged.

ENG-1811 — Web Inspector debug-only

WKWebView.isInspectable was set true unconditionally, leaving the survey WebView inspectable in production.

Fix: wrap in #if DEBUG.


Tests

  • ENG-1810: testWebViewAuthChallengeUsesDefaultHandlingAndDoesNotForceTrust — asserts .performDefaultHandling + nil credential. Verified it fails when the force-trust bug is reintroduced.
  • ENG-1812: testExternalURLSchemeAllowlist — http/https allowed; tel/sms/mailto/facetime/file/javascript/custom-app blocked.
  • ENG-1813: testWebViewPayloadIsBase64EncodedNotRawTemplateLiteral — asserts the raw template literal is gone and the embedded blob is pure base64 that decodes to valid JSON. Existing testWebViewDataUsesSurveyOverwrites updated to decode the base64 payload.
  • ENG-1811: no runtime test — #if DEBUG is a compile-time flag and the test bundle builds in DEBUG, so the release path isn't observable at runtime. Verified by inspection.

Full suite green on the iOS Simulator; no new warnings.

Scope

iOS SDK only. The report also asks to audit the same patterns in Android (onReceivedSslError / handler.proceed()), Flutter, and React Native wrappers — tracked separately.

Acceptance

  • ENG-1810 — WebView rejects invalid/self-signed/mismatched certs by default; no path force-trusts arbitrary certs
  • ENG-1811 — Web Inspector gated behind #if DEBUG
  • ENG-1812 — external URLs restricted to http/https
  • ENG-1813 — payload no longer injectable as JS
  • Regression tests added/updated and passing

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

SurveyWebView’s coordinator no longer creates URL credentials from server trust during TLS authentication challenges. It now always returns .performDefaultHandling, delegating certificate-chain validation to the operating system. A WebKit-based XCTest regression guard constructs an authentication challenge and verifies the default handling result and absence of a force-trusting credential.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the survey WebView hardening work and is relevant to the changeset.
Description check ✅ Passed The description is directly related to the PR and matches the WebView security fixes and tests.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- 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) <noreply@anthropic.com>
@pandeymangg pandeymangg changed the title fix: restores default TLS validation in survey webview fix: harden survey webview — TLS, script injection, URL scheme, inspectable (ENG-1810/1811/1812/1813) Jul 21, 2026
The challenge parameter is unused now that the handler always performs
default TLS handling; name it _ to satisfy SonarQube.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pandeymangg
pandeymangg requested a review from Dhruwang July 21, 2026 07:06
@Dhruwang

Copy link
Copy Markdown
Member

Automated review — summary

Core fixes look correct: TLS .performDefaultHandling restores chain validation, base64 payload round-trip is UTF-8-safe and inert (JSON.parse, not eval), #if DEBUG gate is right. Three of four ship as-is.

One real gap — ENG-1812 (URL allowlist) is incomplete:

isAllowedExternalURL is only consulted in the .onOpenExternalURL JS-bridge branch. The Coordinator has no decidePolicyForNavigationAction, so direct WebView navigation from survey markup (<a href="tel:...">, window.location, meta-refresh, form POST) bypasses the allowlist. WKWebView natively handles system schemes (tel/sms/mailto/facetime) on such navigation — exactly what this fix aims to block. Suggest: enforce the allowlist centrally in decidePolicyForNavigationAction.

Worth adding (P2):

  • Quote-survival test — the \"' mangling removal has no coverage; a regression restoring it would pass the suite.
  • Non-serverTrust TLS test — handler is now unconditional but only serverTrust is exercised.

Minor (P3): UIApplication.open can still launch a native app via universal-link/redirect even for allowed https; base64 encode/decode contract is comment-linked only; duplicated brittle atob(" HTML scan in two tests.

pandeymangg and others added 2 commits July 21, 2026 17:42
…-1812)

Review found the scheme allowlist was only checked in the onOpenExternalURL
JS-bridge path. Direct navigation from survey markup (<a href="tel:...">,
window.location, meta refresh, form POST) bypassed it and reached WKWebView's
native scheme handling.

Add decidePolicyForNavigationAction to the Coordinator: only the in-memory
survey document loads in-frame; every other navigation is cancelled and routed
through the shared openExternalURL helper (http/https opened, everything else
blocked). The allowlist is now enforced on both the bridge and direct navigation.

Tests:
- navigation policy (about:blank/nil load in-frame; web + non-web schemes do not)
- non-serverTrust auth challenges also use .performDefaultHandling
- quotes and angle brackets in survey content survive the base64 round-trip
  (guards the removed \"->' mangling)
- dedupe the atob(...) payload extraction into one helper

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The webView parameter is unused; name it _ to satisfy SonarCloud (same as
the auth-challenge handler).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@pandeymangg
pandeymangg added this pull request to the merge queue Jul 22, 2026
Merged via the queue into main with commit cf2b6ad Jul 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants