Skip to content

WebRTC gating - #379

Open
ERussel wants to merge 9 commits into
mainfrom
feature/web-rtc
Open

WebRTC gating#379
ERussel wants to merge 9 commits into
mainfrom
feature/web-rtc

Conversation

@ERussel

@ERussel ERussel commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

The lockdown container hard-deletes RTCPeerConnection, so products can never
use WebRTC inside the iOS/Android host. This gates it behind an app-level
permission instead: when a native container bridge is present, RTCPeerConnection
is a permission-gated subclass; with no bridge it stays deleted (fail-closed).

SOLUTION

  • js/containerwindow.RTCPeerConnection is a gated subclass. The five
    network-initiating methods (createOffer, createAnswer,
    setLocalDescription, setRemoteDescription, addIceCandidate) request
    allowWebRtcAccess from 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 one
    request/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.
  • Playground extension — a WebRTC method requests camera + microphone (device
    permissions) and WebRTC (remote permission) through TrUAPI, then runs
    getUserMedia + RTCPeerConnection.createOffer. It appears in the method
    list and the diagnosis.

iOS integration

https://github.com/paritytech/polkadot-app-ios-v2/pull/1351

@ERussel
ERussel requested a review from a team August 13, 2026 07:47
@ERussel ERussel changed the title Feature/web rtc WebRTC gating Aug 13, 2026

@filvecchiato filvecchiato left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 are configurable: 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 {} sets G's [[Prototype]] to Native, so freezeValue'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:

  • callNative has no timeout (native-transport.ts:52-59). If the native reply never arrives, the promise never settles and createOffer() 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.Android aren't frozen in index.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() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong composition - bridge should be passed as argument to constructor, so every container consumer must setup provider explicitly

}
});
}
this.connectionClass = GatedRTCPeerConnection;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +15 to +18
export const HANDLER_NAME = '__container__';

/** Global the native side invokes with a request id and JSON reply payload. */
export const CALLBACK_NAME = '__container_callback__';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This values got repeated multiple times in your code, dedupe

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same helper, use nanoid

}

/** Async methods that initiate network activity and must be gated. */
type GatedMethodName =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inverse,

type GatedMethodName = typeof GATED_METHODS[number]


class GatedRTCPeerConnection extends nativeConnectionClass {}

const proto = GatedRTCPeerConnection.prototype;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here - use Proxy instead

Comment thread js/container/README.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants