Android WebView: bridge only on our own https page, external schemes blocked, file access off; payment URL must be http(s)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-05 09:44:47 +03:30
parent e850f6dfbd
commit 1025321b5f
2 changed files with 19 additions and 4 deletions

View File

@@ -40,6 +40,9 @@ class MainActivity : AppCompatActivity() {
private var connection: Connection? = null
private val webHost: String = Uri.parse(BuildConfig.WEB_URL).host ?: ""
/** فقط خودِ سایت و فقط با https؛ «http://دامنه‌ی ما» قابلِ دست‌کاریِ شبکه است و نباید پل داشته باشد. */
private fun isOurPage(uri: Uri?): Boolean = uri != null && uri.scheme == "https" && uri.host == webHost
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -48,12 +51,19 @@ class MainActivity : AppCompatActivity() {
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true // توکنِ ورود در localStorage است
// WebView فقط آدرسِ وب را می‌خواند؛ دسترسی به فایل و content:// لازم نیست.
webView.settings.allowFileAccess = false
webView.settings.allowContentAccess = false
webView.webViewClient = object : WebViewClient() {
// هر آدرسی بیرونِ دامنه‌ی خودمان در مرورگرِ گوشی باز می‌شود، نه داخلِ اپ؛ پس پل فقط
// در صفحه‌های خودمان در دسترس است.
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
if (request.url.host == webHost) return false
startActivity(Intent(Intent.ACTION_VIEW, request.url))
if (isOurPage(request.url)) return false
// فقط http(s) به مرورگرِ گوشی می‌رود؛ intent:، file: و schemeهای سفارشی بی‌صدا بسته می‌شوند
// (پیش‌تر هر آدرسی بیرونِ دامنه به ACTION_VIEW می‌رفت و صفحه می‌توانست اپِ دلخواه باز کند).
if (request.url.scheme == "https" || request.url.scheme == "http") {
startActivity(Intent(Intent.ACTION_VIEW, request.url))
}
return true
}
}
@@ -145,14 +155,17 @@ class MainActivity : AppCompatActivity() {
@JavascriptInterface
fun bazaarPurchase(sku: String, orderId: String) {
runOnUiThread {
if (Uri.parse(webView.url ?: "").host != webHost) return@runOnUiThread
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
purchase(sku, orderId)
}
}
@JavascriptInterface
fun bazaarConsume(purchaseToken: String) {
runOnUiThread { consume(purchaseToken) }
runOnUiThread {
if (!isOurPage(Uri.parse(webView.url ?: ""))) return@runOnUiThread
consume(purchaseToken)
}
}
}

View File

@@ -210,6 +210,8 @@ function CheckoutSheet({ plan, bazaar, onClose, onPaid }: { plan: Plan | null; b
}
}
if (!order.paymentUrl) throw new Error('order without paymentUrl');
// فقط http(s): یک «javascript:» یا «data:» از پاسخِ API نباید با assign اجرا شود.
if (!/^https?:\/\//i.test(order.paymentUrl)) throw new Error('paymentUrl is not http(s)');
window.location.assign(order.paymentUrl);
} catch (err) {
setError(friendlyError(err, 'اتصال به درگاه انجام نشد، دوباره امتحان کن'));