Skip to content

Commit b0af12a

Browse files
SONARKT-619 Rule S7409: Exposing Java interfaces in WebViews is security-sensitive (#591)
1 parent 80ca7c8 commit b0af12a

File tree

9 files changed

+336
-0
lines changed

9 files changed

+336
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package android.annotation;
2+
3+
public @interface JavascriptInterface {
4+
}

kotlin-checks-test-sources/src/main/java/android/webkit/WebView.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
33
public class WebView {
44
public static void setWebContentsDebuggingEnabled(boolean enabled) {
55
}
6+
7+
public void addJavascriptInterface(Object object, String name) {
8+
}
69
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package checks
2+
3+
import android.annotation.JavascriptInterface
4+
import android.webkit.WebView
5+
6+
class AndroidWebViewJavascriptInterfaceCheck {
7+
private val valWebViewProperty: WebView = WebView()
8+
private var varWebViewProperty: WebView = WebView()
9+
10+
class JsObject {
11+
@JavascriptInterface
12+
override fun toString(): String {
13+
return "injectedObject"
14+
}
15+
}
16+
17+
class NotAWebView {
18+
fun addJavascriptInterface(obj: Any?, name: String?) {
19+
}
20+
}
21+
22+
open class WebViewChild : WebView() {
23+
}
24+
25+
class WebViewGrandChild : WebViewChild() {
26+
override fun addJavascriptInterface(obj: Any?, name: String?) {
27+
}
28+
}
29+
30+
fun nonCompliantScenarios(webViewParam: WebView) {
31+
webViewParam.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant {{Exposing a Javascript interface can expose sensitive information to attackers. Make sure it is safe here.}}
32+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
33+
WebView().addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
34+
valWebViewProperty.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
35+
varWebViewProperty.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
36+
val valWebViewLocal = WebView()
37+
valWebViewLocal.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
38+
var varWebViewLocal = WebView()
39+
varWebViewLocal.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
40+
41+
val valJsObjectLocal = JsObject()
42+
webViewParam.addJavascriptInterface(valJsObjectLocal, "injectedObject") // Noncompliant
43+
val valInjectedName = "injectedObject"
44+
webViewParam.addJavascriptInterface(JsObject(), valInjectedName) // Noncompliant
45+
webViewParam.addJavascriptInterface(valJsObjectLocal, valInjectedName) // Noncompliant
46+
47+
val valWebViewClosure = WebView()
48+
val valInjectedNameClosure = "injectedObject"
49+
val valJsObjectLocalClosure = JsObject()
50+
fun closure() {
51+
valWebViewClosure.addJavascriptInterface(valJsObjectLocalClosure, valInjectedNameClosure) // Noncompliant
52+
}
53+
54+
// Complex expressions as parameters
55+
webViewParam.addJavascriptInterface(if (true) JsObject() else JsObject(), "injectedObject") // Noncompliant
56+
webViewParam.addJavascriptInterface(valJsObjectLocal, valInjectedName.toString()) // Noncompliant
57+
webViewParam.addJavascriptInterface(JsObject(), valInjectedName + "suffix") // Noncompliant
58+
webViewParam.let {
59+
it.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
60+
}
61+
webViewParam.let { theWebView ->
62+
theWebView.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
63+
}
64+
webViewParam?.let {
65+
it.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
66+
it?.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
67+
}
68+
webViewParam.run {
69+
addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
70+
}
71+
webViewParam.apply {
72+
addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
73+
this.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
74+
}
75+
with(webViewParam) {
76+
addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
77+
this.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
78+
}
79+
80+
val derivedFromWebView = WebViewChild()
81+
derivedFromWebView.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
82+
83+
val derivedFromWebViewBaseType: WebView = WebViewChild()
84+
derivedFromWebViewBaseType.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
85+
86+
val derivedFromWebViewGrandChild = WebViewGrandChild()
87+
derivedFromWebViewGrandChild.addJavascriptInterface(JsObject(), "injectedObject") // Noncompliant
88+
}
89+
90+
fun compliantScenarios(webView: WebView, notAWebView: NotAWebView) {
91+
WebView() // Compliant, no method invoked on the WebView
92+
WebView().hashCode() // Compliant, different method invoked on the WebView
93+
notAWebView.addJavascriptInterface(JsObject(), "injectedObject") // Compliant, not a WebView
94+
notAWebView.let {
95+
it.addJavascriptInterface(JsObject(), "injectedObject") // Compliant, not a WebView
96+
it?.addJavascriptInterface(JsObject(), "injectedObject") // Compliant, not a WebView
97+
}
98+
with(notAWebView) {
99+
addJavascriptInterface(JsObject(), "injectedObject") // Compliant, not a WebView
100+
this.addJavascriptInterface(JsObject(), "injectedObject") // Compliant, not a WebView
101+
}
102+
}
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.checks
18+
19+
import org.jetbrains.kotlin.analysis.api.resolution.KaFunctionCall
20+
import org.jetbrains.kotlin.psi.KtCallExpression
21+
import org.sonar.check.Rule
22+
import org.sonarsource.kotlin.api.checks.ANY_TYPE
23+
import org.sonarsource.kotlin.api.checks.CallAbstractCheck
24+
import org.sonarsource.kotlin.api.checks.FunMatcher
25+
import org.sonarsource.kotlin.api.checks.STRING_TYPE
26+
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
27+
import org.sonarsource.kotlin.api.visiting.withKaSession
28+
29+
private const val MESSAGE = "Exposing a Javascript interface can expose sensitive information to attackers. Make sure it is safe here."
30+
31+
@Rule(key = "S7409")
32+
class AndroidWebViewJavascriptInterfaceCheck : CallAbstractCheck() {
33+
34+
override val functionsToVisit = listOf(
35+
FunMatcher {
36+
definingSupertype = "android.webkit.WebView"
37+
withNames("addJavascriptInterface")
38+
withArguments(ANY_TYPE, STRING_TYPE)
39+
},
40+
)
41+
42+
override fun visitFunctionCall(
43+
callExpression: KtCallExpression,
44+
resolvedCall: KaFunctionCall<*>,
45+
kotlinFileContext: KotlinFileContext
46+
) = withKaSession {
47+
kotlinFileContext.reportIssue(callExpression, MESSAGE)
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.checks
18+
19+
class AndroidWebViewJavascriptInterfaceCheckTest : CheckTest(AndroidWebViewJavascriptInterfaceCheck())

sonar-kotlin-plugin/src/main/java/org/sonarsource/kotlin/plugin/KotlinCheckList.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.sonarsource.kotlin.checks.AbstractClassShouldBeInterfaceCheck
2020
import org.sonarsource.kotlin.checks.AllBranchesIdenticalCheck
2121
import org.sonarsource.kotlin.checks.AnchorPrecedenceCheck
2222
import org.sonarsource.kotlin.checks.AndroidBroadcastingCheck
23+
import org.sonarsource.kotlin.checks.AndroidWebViewJavascriptInterfaceCheck
2324
import org.sonarsource.kotlin.checks.ArrayHashCodeAndToStringCheck
2425
import org.sonarsource.kotlin.checks.AuthorisingNonAuthenticatedUsersCheck
2526
import org.sonarsource.kotlin.checks.BadClassNameCheck
@@ -154,6 +155,7 @@ val KOTLIN_CHECKS = listOf(
154155
AllBranchesIdenticalCheck::class.java,
155156
AnchorPrecedenceCheck::class.java,
156157
AndroidBroadcastingCheck::class.java,
158+
AndroidWebViewJavascriptInterfaceCheck::class.java,
157159
ArrayHashCodeAndToStringCheck::class.java,
158160
AuthorisingNonAuthenticatedUsersCheck::class.java,
159161
BadClassNameCheck::class.java,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<p>Using Javascript interfaces in WebViews is unsafe as it allows JavaScript to invoke Java methods, potentially giving attackers access to data or
2+
sensitive app functionality. WebViews might include untrusted sources such as third-party iframes, making this functionality particularly risky. As
3+
Javascript interfaces are passed to every frame in the WebView, those iframes are also able to access the exposed Java methods.</p>
4+
<h2>Ask Yourself Whether</h2>
5+
<ul>
6+
<li> The content in the WebView is fully trusted and secure. </li>
7+
<li> Potentially untrusted iframes could be loaded in the WebView. </li>
8+
<li> The Javascript interface has to be exposed for the entire lifecycle of the WebView. </li>
9+
<li> The exposed Java methods will accept input from potentially untrusted sources. </li>
10+
</ul>
11+
<p>There is a risk if you answered yes to any of these questions.</p>
12+
<h2>Recommended Secure Coding Practices</h2>
13+
<h3>Disable JavaScript</h3>
14+
<p>If it is possible to disable JavaScript in the WebView, this is the most secure option. By default, JavaScript is disabled in a WebView, so you do
15+
not need to explicitly call <code>webSettings.setJavaScriptEnabled(true)</code> in your <code>WebSettings</code> configuration. Of course, sometimes
16+
it is necessary to enable JavaScript, in which case the following recommendations should be considered.</p>
17+
<h3>Remove JavaScript interface when loading untrusted content</h3>
18+
<p>JavaScript interfaces can be removed at a later point. It is recommended to remove the JavaScript interface when it is no longer needed. If it is
19+
needed for a longer time, consider removing it before loading untrusted content. This can be done by calling
20+
<code>webView.removeJavascriptInterface("interfaceName")</code>.</p>
21+
<p>A good place to do this is inside the <code>shouldInterceptRequest</code> method of a <code>WebViewClient</code>, where you can check the URL or
22+
resource being loaded and remove the interface if the content is untrusted.</p>
23+
<h3>Alternative methods to implement native bridges</h3>
24+
<p>If a native bridge has to be added to the WebView, and it is impossible to remove it at a later point, consider using an alternative method that
25+
offers more control over the communication flow. <code>WebViewCompat.postWebMessage</code>/<code>WebViewCompat.addWebMessageListener</code> and
26+
<code>WebMessagePort.postMessage</code> offer more ways to validate incoming and outgoing messages, such as by being able to restrict the origins that
27+
can send messages to the JavaScript bridge.</p>
28+
<h2>Sensitive Code Example</h2>
29+
<pre>
30+
class ExampleActivity : AppCompatActivity() {
31+
override fun onCreate(savedInstanceState: Bundle?) {
32+
super.onCreate(savedInstanceState)
33+
34+
val webView = WebView(this)
35+
webView.settings.javaScriptEnabled = true
36+
webView.addJavascriptInterface(JavaScriptBridge(), "androidBridge") // Sensitive
37+
}
38+
39+
inner class JavaScriptBridge {
40+
@JavascriptInterface
41+
fun accessUserData(userId): String {
42+
return getUserData(userId)
43+
}
44+
}
45+
}
46+
</pre>
47+
<h2>Compliant Solution</h2>
48+
<p>The most secure option is to disable JavaScript entirely.</p>
49+
<pre>
50+
class ExampleActivity : AppCompatActivity() {
51+
override fun onCreate(savedInstanceState: Bundle?) {
52+
super.onCreate(savedInstanceState)
53+
54+
val webView = WebView(this)
55+
webView.settings.javaScriptEnabled = false
56+
}
57+
}
58+
</pre>
59+
<p>If possible, remove the JavaScript interface after it is no longer needed, or before loading any untrusted content.</p>
60+
<pre>
61+
class ExampleActivity : AppCompatActivity() {
62+
override fun onCreate(savedInstanceState: Bundle?) {
63+
super.onCreate(savedInstanceState)
64+
65+
val webView = WebView(this)
66+
webView.settings.javaScriptEnabled = true
67+
68+
webView.addJavascriptInterface(JavaScriptBridge(), "androidBridge")
69+
70+
// Sometime later, before unsafe content is loaded, remove the JavaScript interface
71+
webView.removeJavascriptInterface("androidBridge")
72+
}
73+
}
74+
</pre>
75+
<p>If a JavaScript bridge must be used, consider using <code>WebViewCompat.addWebMessageListener</code> instead. This allows you to restrict the
76+
origins that can send messages to the JavaScript bridge.</p>
77+
<pre>
78+
class ExampleActivity : AppCompatActivity() {
79+
private val ALLOWED_ORIGINS = setOf("https://example.com")
80+
81+
override fun onCreate(savedInstanceState: Bundle?) {
82+
super.onCreate(savedInstanceState)
83+
84+
val webView = WebView(this)
85+
webView.settings.javaScriptEnabled = true
86+
87+
WebViewCompat.addWebMessageListener(
88+
webView, "androidBridge", ALLOWED_ORIGINS, // Only allow messages from these origins
89+
object : WebViewCompat.WebMessageListener {
90+
override fun onPostMessage(
91+
view: WebView,
92+
message: WebMessageCompat,
93+
sourceOrigin: Uri,
94+
isMainFrame: Boolean,
95+
replyProxy: JavaScriptReplyProxy
96+
) {
97+
// Handle the message
98+
}
99+
}
100+
)
101+
}
102+
}
103+
</pre>
104+
<h2>See</h2>
105+
<ul>
106+
<li> Android Documentation - <a href="https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges">Insecure WebView
107+
native bridges</a> </li>
108+
<li> Android Documentation - <a href="https://developer.android.com/reference/androidx/webkit/WebViewCompat">WebViewCompat API reference</a> </li>
109+
<li> OWASP - <a href="https://owasp.org/Top10/A05_2021-Security_Misconfiguration/">Top 10 2021 Category A5 - Security Misconfiguration</a> </li>
110+
<li> OWASP - <a href="https://owasp.org/www-project-mobile-top-10/2023-risks/m4-insufficient-input-output-validation.html">Mobile Top 10 2024
111+
Category M4 - Insufficient Input/Output Validation</a> </li>
112+
<li> OWASP - <a href="https://owasp.org/www-project-mobile-top-10/2023-risks/m8-security-misconfiguration.html">Mobile Top 10 2024 Category M8 -
113+
Security Misconfiguration</a> </li>
114+
<li> CWE - <a href="https://cwe.mitre.org/data/definitions/79">CWE-79 - Improper Neutralization of Input During Web Page Generation</a> </li>
115+
</ul>
116+
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"title": "Exposing Java interfaces in WebViews is security-sensitive",
3+
"type": "SECURITY_HOTSPOT",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "30min"
8+
},
9+
"tags": [
10+
"cwe",
11+
"android"
12+
],
13+
"defaultSeverity": "Major",
14+
"ruleSpecification": "RSPEC-7409",
15+
"sqKey": "S7409",
16+
"scope": "All",
17+
"securityStandards": {
18+
"OWASP Mobile": [
19+
"M1"
20+
],
21+
"OWASP Mobile Top 10 2024": [
22+
"M4",
23+
"M8"
24+
],
25+
"OWASP Top 10 2021": [
26+
"A5"
27+
],
28+
"CWE": [
29+
79
30+
]
31+
},
32+
"quickfix": "unknown",
33+
"code": {
34+
"impacts": {
35+
"SECURITY": "MEDIUM"
36+
},
37+
"attribute": "TRUSTWORTHY"
38+
}
39+
}

sonar-kotlin-plugin/src/main/resources/org/sonar/l10n/kotlin/rules/kotlin/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"S6631",
128128
"S6634",
129129
"S7204",
130+
"S7409",
130131
"S7416"
131132
]
132133
}

0 commit comments

Comments
 (0)