WebRTC gating - #379
Conversation
filvecchiato
left a comment
There was a problem hiding this comment.
Architecture looks right: decision stays in the core (iOS peeks permissionAuthorizationStatus, no prompting), container owns interception, fail-closed with no bridge. The reply-forgery hardening — 128-bit ids, postMessage captured pre-lockdown, frozen callback — covers the hard attack.
One blocking issue.
The gate can be removed from product code. WebRtcManager shadows the five methods on a subclass prototype and leaves the native class reachable. Reproduced with the requester hardwired to deny — all three succeed:
delete RTCPeerConnection.prototype.createOffer— props areconfigurable: true, writable: true(webrtc-manager.ts:96-97), so the shadow deletes and the native method is exposed.Object.getPrototypeOf(Object.getPrototypeOf(pc)).createOffer.call(pc)— the instance is a native peer connection; the parent prototype still holds the ungated method.new (Object.getPrototypeOf(window.RTCPeerConnection))()—class G extends Native {}setsG's[[Prototype]]toNative, sofreezeValue's getter hands back the native constructor.
Blocking because the threat model is same-realm product script (freeze.ts: must not "reach, replace, or wrap"). The old freezeAndDelete had no such hole — no constructor meant no reachable prototype.
Fix: patch the native prototype in place with configurable: false, writable: false and install that class directly, instead of subclassing. No shadow to delete, no ungated parent, no recoverable constructor. The product realm is the webview's only occupant, so there's no host-side collateral.
The five gated methods are the right chokepoints — data channels, tracks and ICE all still need setLocalDescription. It's the installation, not the policy. Could those three cases go into webrtc-manager.test.ts? Current tests are happy/deny path only.
Non-blocking:
callNativehas no timeout (native-transport.ts:52-59). If the native reply never arrives, the promise never settles andcreateOffer()neither resolves nor rejects. Safe, but indistinguishable from a hang.- Grants cache per connection (
webrtc-manager.ts:73), so a long-lived connection survives revocation. Since the gate is a pure peek, re-checking per call is nearly free — worth making the choice explicit. window.webkit/window.Androidaren't frozen inindex.ts, so product code can call__container__directly.
Scope: RemotePermission::WebRtc is still un-enforced in the core, so desktop/dotli get nothing here (dotli auto-grants outright). Consistent with request-on-use being a separate task. #372 covers moving the decision into the core; probably worth cross-referencing.
| } | ||
|
|
||
| // src/native-transport.ts | ||
| function randomId() { |
There was a problem hiding this comment.
use nanoid library instead
| * Returns the transport for the first available native bridge — iOS first, then | ||
| * Android — or `undefined` when neither host is present (fail-closed). | ||
| */ | ||
| export function createNativeBridge(): NativeTransport | undefined { |
There was a problem hiding this comment.
Wrong composition - bridge should be passed as argument to constructor, so every container consumer must setup provider explicitly
| } | ||
| }); | ||
| } | ||
| this.connectionClass = GatedRTCPeerConnection; |
There was a problem hiding this comment.
solve this problem with Proxy. constructor is basically a function that creates object and attaches prototype to it. You can write
constructor() {
return new Proxy(this, handler);
}and route intercept all methods access through reflection
| export const HANDLER_NAME = '__container__'; | ||
|
|
||
| /** Global the native side invokes with a request id and JSON reply payload. */ | ||
| export const CALLBACK_NAME = '__container_callback__'; |
There was a problem hiding this comment.
This values got repeated multiple times in your code, dedupe
There was a problem hiding this comment.
It is defined once in that file and reused by export
| /** A request/response channel to the native host. */ | ||
| export interface NativeTransport { | ||
| /** Sends a request and resolves with the native `value` (or rejects on error). */ | ||
| callNative(method: string, params: unknown): Promise<unknown>; |
There was a problem hiding this comment.
Not for this fix, but idea - use SCALE codecs that now available on both sides to serialize messages, instead of free-formed objects that might or might not be stringified to JSON. It will hit when first non-serializable values will appear, like BigInt or Dates (or whatever else)
| } | ||
|
|
||
| /** 128-bit random hex id; unguessable so replies cannot be forged by id. */ | ||
| function randomId(): string { |
There was a problem hiding this comment.
same helper, use nanoid
| } | ||
|
|
||
| /** Async methods that initiate network activity and must be gated. */ | ||
| type GatedMethodName = |
There was a problem hiding this comment.
inverse,
type GatedMethodName = typeof GATED_METHODS[number]
|
|
||
| class GatedRTCPeerConnection extends nativeConnectionClass {} | ||
|
|
||
| const proto = GatedRTCPeerConnection.prototype; |
There was a problem hiding this comment.
same here - use Proxy instead
There was a problem hiding this comment.
Slopidy-slop, ask Claude to rewrite it using next structure
- Context
- Problem that we're solving with this library
- Solution, with code examples and bootstrap guide
SUMMARY
The lockdown container hard-deletes
RTCPeerConnection, so products can neveruse WebRTC inside the iOS/Android host. This gates it behind an app-level
permission instead: when a native container bridge is present,
RTCPeerConnectionis a permission-gated subclass; with no bridge it stays deleted (fail-closed).
SOLUTION
js/container—window.RTCPeerConnectionis a gated subclass. The fivenetwork-initiating methods (
createOffer,createAnswer,setLocalDescription,setRemoteDescription,addIceCandidate) requestallowWebRtcAccessfrom the host once per connection over a__container__message-handler bridge; denial closes the connection and throws. iOS
(
window.webkit.messageHandlers) and Android (window.Android) share onerequest/response transport. Same-realm spoof resistance: the native sender is
captured at init, request ids are 128-bit random, and the reply callback is
frozen.
WebRTCmethod requests camera + microphone (devicepermissions) and WebRTC (remote permission) through TrUAPI, then runs
getUserMedia+RTCPeerConnection.createOffer. It appears in the methodlist and the diagnosis.
iOS integration
https://github.com/paritytech/polkadot-app-ios-v2/pull/1351