Implementing Isolated Sandbox for Mini-Programs in Super App
A Super App with mini-programs is essentially an operating system within an operating system. The host application loads and executes code from third-party developers. If this code can read data from other mini-programs or the main app — the entire security architecture falls apart.
Why This Is More Complex Than It Seems
WeChat Mini Programs, Grab SuperApp, Gojek — each has its own isolation implementation. Main problem: native code in iOS and Android can't "isolate" arbitrary JS or Dart code without special mechanisms. WebView provides DOM isolation but not memory isolation or network request limitation.
Typical anti-pattern: load mini-program JS in WKWebView / WebView, open addJavascriptInterface for needed APIs — and think it's a sandbox. It's not. Any XSS in the mini-program gets access to all objects registered via addJavascriptInterface, including bridges to native code.
Isolation Levels to Implement
1. Code Execution Isolation
On Android, mini-programs on JS are better executed in a separate process via android:process attribute in manifest. Each mini-program — separate process with its own heap. One program crashing doesn't take down the host. For Dart/Flutter — Isolate with limited ReceivePort API.
For WebView-based mini-programs: WebView with setJavaScriptEnabled(true) in separate process + WebViewClient with allowlist of hosts:
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 // continue
}
private fun blockRequest() = WebResourceResponse(
"text/plain", "UTF-8", ByteArrayInputStream("blocked".toByteArray())
)
}
2. JavaScript Bridge with Capability Model
Instead of open addJavascriptInterface — declarative bridge with explicit permissions list. Mini-program requests an API, host checks if it's allowed in this program's manifest:
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)
}
}
Mini-program manifest describes requested APIs — like uses-permission in Android, but for mini app ecosystem.
3. Storage Isolation
Each mini-program gets isolated namespace in SharedPreferences and separate directory in filesDir:
/app/mini_programs/
/{mini_app_id}/
/storage/ ← SharedPreferences namespace
/files/ ← file storage
/cache/ ← cleared when space runs low
Access to another mini-program's storage — only via explicit Intent with user confirmation. Cross-program data access outside this scheme — forbidden at ContentProvider level with callingUid check.
4. Network Isolation
On Android 8+ use ConnectivityManager with NetworkCapabilities to bind specific connection to VPN profile of mini-program. Less aggressive variant — proxy with allowlist at host level and HTTPS pinning to mini-program servers via custom X509TrustManager.
On iOS — WKContentWorld (iOS 14+) lets each mini-program execute JS in isolated world with separate global object:
let miniAppWorld = WKContentWorld.world(withName: "mini_app_\(miniAppId)")
webView.evaluateJavaScript(miniAppCode, in: nil, in: miniAppWorld) { result, error in
// code executes in isolated context
}
Different WKContentWorld instances can't see each other's variables even in one WKWebView.
Mini-Program Code Attestation
Before execution — verify bundle signature. Each bundle is signed by developer and checked against public key registered on platform:
fun verifyMiniAppBundle(bundle: ByteArray, signature: ByteArray, publisherKey: PublicKey): Boolean {
val sig = Signature.getInstance("SHA256withECDSA")
sig.initVerify(publisherKey)
sig.update(bundle)
return sig.verify(signature)
}
Running unsigned or modified bundle — denial with incident logging.
Runtime Monitoring
Sandbox is not static. Need runtime monitoring: CPU time per mini-program, allocated memory volume, network requests count. Mini-program making 500 requests per second is either broken or mining.
On Android — Debug.MemoryInfo + Debug.ThreadCpuTimeNanos() for each mini-program process. Thresholds configured in platform config.
Timeline
Basic sandbox with WebView process isolation and capability bridge — 2–3 weeks. Full platform with network isolation, bundle attestation, runtime monitoring and permission management console — 2–3 months.







