Реалізація ізольованої пісочниці (Sandbox) для мініпрограм у Super App
Super App з мініпрограмами — це, по суті, операційна система всередині операційної системи. Хост-додаток завантажує та виконує код від сторонніх розробників. Якщо цей код може читати дані інших мініпрограм або основного додатку — вся архітектура безпеки розваліється.
Чому це складніше, ніж здається
У WeChat Mini Programs, Grab SuperApp, Gojek — скрізь своя реалізація ізоляції. Головна проблема: нативний код у iOS та Android не вміє «ізолювати» довільний JS чи Dart код без спеціальних механізмів. WebView дає ізоляцію DOM, але не ізоляцію пам'яти та не обмеження мережевих запитів.
Типовий антипаттерн: завантажити JS мініпрограми в WKWebView / WebView, відкрити addJavascriptInterface для потрібних API — і вважати це пісочницею. Це не пісочниця. Будь-який XSS у мініпрограмі отримує доступ до всіх об'єктів, зареєстрованих через addJavascriptInterface, включаючи мости до нативного коду.
Рівні ізоляції, які потрібно реалізувати
1. Ізоляція виконання коду
На Android мініпрограми на JS краще виконувати у окремому процесі через атрибут android:process у маніфесті. Кожна мініпрограма — окремий процес зі своєю heap. Крах однієї програми не валить хост. Для Dart/Flutter — Isolate з обмеженим API ReceivePort.
Для WebView-базованих мініпрограм: WebView з setJavaScriptEnabled(true) у окремому процесі + WebViewClient із білим списком хостів:
class SandboxedWebViewClient(
private val allowedHosts: Set<String>
) : WebViewClient() {
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest
): WebResourceResponse? {
val host = request.url.host ?: return blockRequest()
if (host !in allowedHosts) {
auditLogger.logBlockedRequest(miniProgramId, request.url)
return blockRequest()
}
return null // продовжуємо
}
private fun blockRequest() = WebResourceResponse(
"text/plain", "UTF-8", ByteArrayInputStream("blocked".toByteArray())
)
}
2. JavaScript Bridge із capability model
Замість відкритого addJavascriptInterface — декларативний міст із явним списком розрішень. Мініпрограма запитує API, хост перевіряє, дозволено ли він у маніфесті цієї програми:
class CapabilityBridge(
private val miniAppManifest: MiniAppManifest,
private val userId: String
) {
@JavascriptInterface
fun callNative(apiName: String, params: String, callbackId: String) {
val capability = Capability.fromString(apiName) ?: run {
sendError(callbackId, "UNKNOWN_API")
return
}
if (!miniAppManifest.hasPermission(capability)) {
auditLogger.logUnauthorizedApiCall(miniAppId, apiName)
sendError(callbackId, "PERMISSION_DENIED")
return
}
nativeApiRouter.dispatch(capability, params, callbackId)
}
}
Маніфест мініпрограми описує запрошені API — аналог uses-permission в Android, тільки для екосистеми mini app.
3. Ізоляція сховища
Кожна мініпрограма отримує ізольований namespace у SharedPreferences та окремої директорії в filesDir:
/app/mini_programs/
/{mini_app_id}/
/storage/ ← SharedPreferences namespace
/files/ ← файлове сховище
/cache/ ← очищується при нехватці місця
Доступ до сховища іншої мініпрограми — тільки через явний Intent із підтвердженням користувача. Кросс-програмний доступ до даних поза цією схемою — заборонений на рівні ContentProvider із перевіркою callingUid.
4. Мережева ізоляція
На Android 8+ можна використовувати ConnectivityManager з NetworkCapabilities для привʾязки конкретного з'єднання до VPN-профілю мініпрограми. Менш агресивний варіант — proxy із allowlist на рівні хоста та HTTPS pinning до серверів мініпрограми через користувацький X509TrustManager.
На iOS — WKContentWorld (iOS 14+) дозволяє виконувати JS кожної мініпрограми у ізольованому світі з окремим глобальним об'єктом:
let miniAppWorld = WKContentWorld.world(withName: "mini_app_\(miniAppId)")
webView.evaluateJavaScript(miniAppCode, in: nil, in: miniAppWorld) { result, error in
// код виконується в ізольованому контексті
}
Різні WKContentWorld не видять змінні одна одної навіть в одному WKWebView.
Аттестація коду мініпрограм
Перед запуском — верифікація підпису бандла. Кожен бандл підписується розробником та перевіряється за публічним ключем, зареєстрованим на платформі:
fun verifyMiniAppBundle(bundle: ByteArray, signature: ByteArray, publisherKey: PublicKey): Boolean {
val sig = Signature.getInstance("SHA256withECDSA")
sig.initVerify(publisherKey)
sig.update(bundle)
return sig.verify(signature)
}
Запуск непідписаного або модифікованого бандла — відмова з логуванням інциденту.
Мониторинг під час виконання
Sandbox — не статична конструкція. Потрібен runtime мониторинг: час виконання CPU для кожної мініпрограми, обʼєм виділеної пам'яті, кількість мережевих запитів. Мініпрограма, що робить 500 запитів на секунду, або зламана, або займається майнингом.
На Android — Debug.MemoryInfo + Debug.ThreadCpuTimeNanos() для кожного процесу мініпрограми. Пороги налаштовуються в конфігу платформи.
Терміни
Базова пісочниця із WebView-процесною ізоляцією та capability bridge — 2–3 тижні. Повна платформа з мережевою ізоляцією, аттестацією бандлів, runtime мониторингом та консоллю управління розрішеннями — 2–3 місяці.







