Skip to content

[TON-1601] Improve iframe security#117

Open
ProudOfZiggy wants to merge 7 commits into
mainfrom
chore/injection-tests
Open

[TON-1601] Improve iframe security#117
ProudOfZiggy wants to merge 7 commits into
mainfrom
chore/injection-tests

Conversation

@ProudOfZiggy

@ProudOfZiggy ProudOfZiggy commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added iframe security investigation and testing tools with multiple test case scenarios.
    • Added security diagnostics logging to monitor iframe operations and origin validation.
    • Added WebView-based dApp testing interface for iframe security testing.
  • Security Improvements

    • Enhanced per-frame origin validation for iframe bridge message handling.
    • Improved frame security enforcement to block opaque-origin subframes.

@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

TonConnectInjector is updated to authenticate per-frame bridge messages using WebMessageListener, enforcing sourceOrigin/isMainFrame and rejecting opaque-origin subframes with a 403 error response. The demo adds an iframesec package containing a Compose security log model, a JavaScript iframe test-case matrix enum (RealBridgeIframeCase), and a diagnostic screen (WalletKitRealBridgeIframeScreen), all wired into the investigation menu via updated navigation state.

Changes

Iframe Origin Enforcement and Demo Security Matrix

Layer / File(s) Summary
SDK: per-frame origin enforcement in TonConnectInjector
TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt
setup() registers WebMessageListener (with legacy addJavascriptInterface fallback) so each bridge message carries authenticated sourceOrigin and isMainFrame. Opaque-origin subframes are rejected with a new 403 constant. handleBridgeMessage becomes origin-aware; handleBridgeRequest resolves resolvedOrigin for dApp URL, and a shared errorResponse(message, code) helper is used across all failure paths. The document-start injection script retries waiting for the JS interface before calling injectWalletKit.
Demo: IframeSecurityLog data model
AndroidDemo/.../iframesec/IframeSecurityLog.kt
Adds IframeSecLogEntry data class (id, timestamp, frameLabel, action, claimed/actual origin, isMainFrame, payload, isNative) and a Compose mutableStateListOf-backed IframeSecLog container with add, addNative (labels entry as "SDK EVENT"), and clear.
Demo: RealBridgeIframeCase test matrix enum and JS generation
AndroidDemo/.../iframesec/RealBridgeIframeCase.kt
Defines eight iframe test scenarios as an enum with metadata (shortTitle, title, summary, expectsSheet, expectation). spawnJs() generates case-specific JavaScript that injects iframes and real signData scripts. Companion object provides DAPP_URL/DAPP_ORIGIN/SAME_ORIGIN_URL constants, realSendScript() JS generator, self-contained HTML/data URL builders, and jsString() escaping.
Demo: WalletKitRealBridgeIframeScreen diagnostic UI
AndroidDemo/.../iframesec/WalletKitRealBridgeIframeScreen.kt
Adds a Compose screen that loads the dApp in a WebView via injectTonConnect, registers an "iframeSecLog" WebMessageListener to capture per-frame origin diagnostics, collects TONWalletKitEvents as native log entries, and provides a case-selector with an "Inject" button that runs selected.spawnJs(). LogRow badges entries by origin-match status; OriginRow renders monospace origin values.
Demo: navigation wiring for new screens
AndroidDemo/.../WalletKitInvestigationScreen.kt, AndroidDemo/.../WalletScreen.kt
WalletKitInvestigationScreen gains an ITONWalletKit parameter and replaces the boolean showTonconnect flow with an InvestigationPage sealed state routing to Tonconnect, DappWebView, and RealBridge. Two new menu rows are added. A private DappWebViewScreen composable hosts a JS-enabled WebView loading RealBridgeIframeCase.DAPP_URL. WalletScreen passes walletKit into the updated call.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(173, 216, 230, 0.5)
    Note over WalletKitRealBridgeIframeScreen,TonConnectInjector: Demo screen injects iframe test case
  end
  participant Screen as WalletKitRealBridgeIframeScreen
  participant WebView
  participant TonConnectInjector
  participant WebMessageListener as WebMessageListener (iframeSecLog)
  participant IframeSecLog

  Screen->>WebView: evaluateJavascript(selected.spawnJs())
  WebView->>TonConnectInjector: postMessage(bridgeMsg, sourceOrigin, isMainFrame)
  TonConnectInjector->>TonConnectInjector: handleBridgeRequest(resolvedOrigin)
  alt opaque-origin subframe
    TonConnectInjector-->>WebView: errorResponse(403)
    WebView->>WebMessageListener: iframeSecLog(frameLabel, action, claimedOrigin, sourceOrigin)
    WebMessageListener->>IframeSecLog: add(entry, isNative=false)
  else authorized frame
    TonConnectInjector-->>Screen: TONWalletKitEvent emitted
    Screen->>IframeSecLog: addNative(action, domain)
    WebView->>WebMessageListener: iframeSecLog(frameLabel, action, claimedOrigin, sourceOrigin)
    WebMessageListener->>IframeSecLog: add(entry, isNative=false)
  end
  IframeSecLog-->>Screen: entries updated → LogRow rendered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A bunny hops through iframe lanes,
checking origins, catching feigns.
Each subframe posts its secret name—
the bridge says "403, no claim!"
The log lights up with badges bright,
opaque frames banished from the site. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.93% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly relates to the main objective of the changeset, which is to enhance iframe security by adding security testing infrastructure, origin validation, and improved WebView message handling.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/injection-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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt (1)

470-484: ⚠️ Potential issue | 🟡 Minor

Consider explicitly removing the WebMessageListener in cleanup().

The addWebMessageListener registered in setup() (line 221) should be paired with an explicit removal in cleanup(). While the code currently relies on the listener being implicitly replaced if a new TonConnectInjector is set up on the same WebView, calling WebViewCompat.removeWebMessageListener(webView, BrowserConstants.JS_INTERFACE_NAME) in cleanup() is more explicit and prevents potential issues with WebView reuse:

override fun cleanup() {
    if (isCleanedUp) {
        return
    }
    isCleanedUp = true

    Logger.d(TAG, "Cleaning up TonConnect injector (browser closed, but session remains active)")

    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
        WebViewCompat.removeWebMessageListener(webView, BrowserConstants.JS_INTERFACE_NAME)
    }

    scope.cancel()
    pendingRequests.clear()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt`
around lines 470 - 484, The cleanup() method does not explicitly remove the
WebMessageListener that was registered in the setup() method. Add an explicit
removal of the WebMessageListener in the cleanup() method by calling
WebViewCompat.removeWebMessageListener() with the webView and
BrowserConstants.JS_INTERFACE_NAME parameters, wrapped in a
WebViewFeature.isFeatureSupported() check to ensure compatibility. Place this
removal before the scope.cancel() call to ensure proper cleanup order.
🧹 Nitpick comments (4)
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt (2)

287-287: 💤 Low value

SimpleDateFormat is not thread-safe.

SimpleDateFormat instances are not thread-safe and shouldn't be shared. While Compose UI runs on the main thread, consider using a local instance in the composable or using java.time.format.DateTimeFormatter which is thread-safe.

For demo code this is low risk, but a quick fix would be:

private fun formatTime(timestamp: Long): String =
    SimpleDateFormat("HH:mm:ss.SSS", Locale.US).format(Date(timestamp))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt`
at line 287, The `timeFormat` property is declared as a shared class-level
instance of `SimpleDateFormat`, which is not thread-safe. Remove this property
and create a private helper function that instantiates a new `SimpleDateFormat`
locally each time formatting is needed, or use
`java.time.format.DateTimeFormatter` as a thread-safe alternative. Update all
usages of `timeFormat` to call this new function instead of accessing the shared
instance directly.

178-222: ⚡ Quick win

Missing cleanupTonConnect() on WebView disposal in both diagnostic screens. Both AndroidView usages call injectTonConnect(walletKit) but never call cleanupTonConnect() when the composable leaves composition, which leaks the injector's coroutine scope and pending requests map.

  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt#L178-L222: Add onRelease = { it.cleanupTonConnect() } to the AndroidView.
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/WalletKitInvestigationScreen.kt#L172-L201: Add onRelease = { it.cleanupTonConnect() } to the AndroidView.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt`
around lines 178 - 222, Both AndroidView composables call
injectTonConnect(walletKit) but are missing cleanup when the composable leaves
composition, causing a resource leak of the injector's coroutine scope and
pending requests map. Add the onRelease parameter to each AndroidView factory
lambda to call cleanupTonConnect() on disposal. At
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt
lines 178-222, add onRelease = { it.cleanupTonConnect() } to the AndroidView
composable that contains the injectTonConnect(walletKit) call. At
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/WalletKitInvestigationScreen.kt
lines 172-201, similarly add onRelease = { it.cleanupTonConnect() } to the
AndroidView composable that contains its injectTonConnect(walletKit) call.
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/RealBridgeIframeCase.kt (1)

349-363: 💤 Low value

Consider escaping </script> sequences in jsString().

If the input string contains </script>, the generated JS embedded in HTML could prematurely close the script tag. For demo diagnostic code this is low risk, but adding s.replace("</", "<\\/") would make it more robust.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/RealBridgeIframeCase.kt`
around lines 349 - 363, The jsString function does not escape forward slash
sequences that could prematurely close script tags when the generated JavaScript
is embedded in HTML. Add a string replacement in the jsString function to escape
`</` sequences by converting them to `<\/` before or after the existing
character-by-character escaping logic. This will prevent any input containing
`</script>` or similar sequences from breaking out of the HTML script tag
prematurely.
TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt (1)

274-286: 💤 Low value

Retry loop executes 51 iterations, not 50.

The condition tries++ > 50 uses post-increment, so it evaluates as tries > 50 after incrementing. This means tries reaches 51 before the condition is true (0→1→...→51). Minor off-by-one if the intent was exactly 50 retries.

If intentional (101ms max wait), consider clarifying the comment or using tries++ >= 50.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt`
around lines 274 - 286, The retry loop condition in the setInterval callback
uses post-increment with `tries++ > 50`, which allows 51 iterations instead of
50 because the condition is evaluated after incrementing. To fix this, either
change the condition to `tries++ >= 50` if exactly 50 retries are intended, or
add a clarifying comment explaining that 51 iterations (approximately 101ms
maximum wait time) is intentional. The loop polls for the JS_INTERFACE_NAME to
be defined, so ensure whichever approach you choose aligns with the intended
polling behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt`:
- Around line 470-484: The cleanup() method does not explicitly remove the
WebMessageListener that was registered in the setup() method. Add an explicit
removal of the WebMessageListener in the cleanup() method by calling
WebViewCompat.removeWebMessageListener() with the webView and
BrowserConstants.JS_INTERFACE_NAME parameters, wrapped in a
WebViewFeature.isFeatureSupported() check to ensure compatibility. Place this
removal before the scope.cancel() call to ensure proper cleanup order.

---

Nitpick comments:
In
`@AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/RealBridgeIframeCase.kt`:
- Around line 349-363: The jsString function does not escape forward slash
sequences that could prematurely close script tags when the generated JavaScript
is embedded in HTML. Add a string replacement in the jsString function to escape
`</` sequences by converting them to `<\/` before or after the existing
character-by-character escaping logic. This will prevent any input containing
`</script>` or similar sequences from breaking out of the HTML script tag
prematurely.

In
`@AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt`:
- Line 287: The `timeFormat` property is declared as a shared class-level
instance of `SimpleDateFormat`, which is not thread-safe. Remove this property
and create a private helper function that instantiates a new `SimpleDateFormat`
locally each time formatting is needed, or use
`java.time.format.DateTimeFormatter` as a thread-safe alternative. Update all
usages of `timeFormat` to call this new function instead of accessing the shared
instance directly.
- Around line 178-222: Both AndroidView composables call
injectTonConnect(walletKit) but are missing cleanup when the composable leaves
composition, causing a resource leak of the injector's coroutine scope and
pending requests map. Add the onRelease parameter to each AndroidView factory
lambda to call cleanupTonConnect() on disposal. At
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt
lines 178-222, add onRelease = { it.cleanupTonConnect() } to the AndroidView
composable that contains the injectTonConnect(walletKit) call. At
AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/WalletKitInvestigationScreen.kt
lines 172-201, similarly add onRelease = { it.cleanupTonConnect() } to the
AndroidView composable that contains its injectTonConnect(walletKit) call.

In
`@TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt`:
- Around line 274-286: The retry loop condition in the setInterval callback uses
post-increment with `tries++ > 50`, which allows 51 iterations instead of 50
because the condition is evaluated after incrementing. To fix this, either
change the condition to `tries++ >= 50` if exactly 50 retries are intended, or
add a clarifying comment explaining that 51 iterations (approximately 101ms
maximum wait time) is intentional. The loop polls for the JS_INTERFACE_NAME to
be defined, so ensure whichever approach you choose aligns with the intended
polling behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b403d16-855b-42c8-a1dd-ae34f21026cf

📥 Commits

Reviewing files that changed from the base of the PR and between cdbcc06 and 5add627.

📒 Files selected for processing (6)
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/WalletKitInvestigationScreen.kt
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/WalletScreen.kt
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/IframeSecurityLog.kt
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/RealBridgeIframeCase.kt
  • AndroidDemo/app/src/main/java/io/ton/walletkit/demo/presentation/ui/screen/iframesec/WalletKitRealBridgeIframeScreen.kt
  • TONWalletKit-Android/impl/src/main/java/io/ton/walletkit/browser/TonConnectInjector.kt

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