@@ -120,6 +120,103 @@ function setupExternalLinkHandlers(window: BrowserWindow): void {
120120 } ) ;
121121}
122122
123+ // The authoritative gate for the in-app browser guest: main process, where a
124+ // guest page can't route around it. The renderer's normalizeAddress is only a
125+ // convenience on top of this.
126+ const ALLOWED_WEBVIEW_SCHEMES = new Set ( [ "http:" , "https:" , "about:" ] ) ;
127+
128+ // The link-local range (incl. cloud metadata 169.254.169.254) can hand out
129+ // instance credentials. Loopback and LAN are deliberately allowed — reaching a
130+ // local dev server is a first-class use of a coding tool's browser.
131+ function isBlockedWebviewHost ( hostname : string ) : boolean {
132+ return / ^ 1 6 9 \. 2 5 4 \. / . test ( hostname ) ;
133+ }
134+
135+ function safeProtocol ( url : string ) : string {
136+ try {
137+ return new URL ( url ) . protocol ;
138+ } catch {
139+ return "" ;
140+ }
141+ }
142+
143+ function isAllowedWebviewNavigation ( url : string ) : boolean {
144+ let parsed : URL ;
145+ try {
146+ parsed = new URL ( url ) ;
147+ } catch {
148+ return false ;
149+ }
150+ return (
151+ ALLOWED_WEBVIEW_SCHEMES . has ( parsed . protocol ) &&
152+ ! isBlockedWebviewHost ( parsed . hostname )
153+ ) ;
154+ }
155+
156+ // The guest runs on a shared persisted profile, so a single grant would stick
157+ // across every tab and task — deny powerful permissions outright.
158+ const DENIED_WEBVIEW_PERMISSIONS = new Set ( [
159+ "media" , // camera + microphone
160+ "geolocation" ,
161+ "notifications" ,
162+ "midi" ,
163+ "midiSysex" ,
164+ "hid" ,
165+ "serial" ,
166+ "usb" ,
167+ "pointerLock" ,
168+ "idle-detection" ,
169+ "openExternal" , // popups are already routed through our own handler
170+ ] ) ;
171+
172+ // Hardens <webview> guests used by the in-app browser tab. The guest renders
173+ // arbitrary untrusted web content inside a privileged app window.
174+ function setupWebviewHandlers ( window : BrowserWindow ) : void {
175+ // Strip any preload / node access an attacker page might request.
176+ window . webContents . on ( "will-attach-webview" , ( _event , webPreferences ) => {
177+ webPreferences . preload = undefined ;
178+ webPreferences . nodeIntegration = false ;
179+ webPreferences . contextIsolation = true ;
180+ } ) ;
181+
182+ window . webContents . on ( "did-attach-webview" , ( _event , guest ) => {
183+ // Deny at both request time (prompts) and check time (sync fast-paths like
184+ // navigator.permissions.query).
185+ guest . session . setPermissionRequestHandler ( ( _wc , permission , callback ) => {
186+ callback ( ! DENIED_WEBVIEW_PERMISSIONS . has ( permission ) ) ;
187+ } ) ;
188+ guest . session . setPermissionCheckHandler (
189+ ( _wc , permission ) => ! DENIED_WEBVIEW_PERMISSIONS . has ( permission ) ,
190+ ) ;
191+
192+ guest . setWindowOpenHandler ( ( { url } ) => {
193+ // http(s)-only: a hostile page must not launch external protocol
194+ // handlers (smb:, file:, custom app URIs) via window.open.
195+ if ( / ^ h t t p s ? : $ / i. test ( safeProtocol ( url ) ) ) {
196+ shell . openExternal ( url ) ;
197+ } else {
198+ log . warn ( "Blocked webview popup to non-http(s) target" , { url } ) ;
199+ }
200+ return { action : "deny" } ;
201+ } ) ;
202+
203+ const guard = (
204+ event : { preventDefault : ( ) => void } ,
205+ url : string ,
206+ ) : void => {
207+ if ( ! isAllowedWebviewNavigation ( url ) ) {
208+ event . preventDefault ( ) ;
209+ log . warn ( "Blocked disallowed webview navigation" , { url } ) ;
210+ }
211+ } ;
212+ // will-navigate + will-redirect cover top-level loads and redirect chains
213+ // (the SSRF-to-metadata vector); will-frame-navigate covers sub-frames.
214+ guest . on ( "will-navigate" , guard ) ;
215+ guest . on ( "will-redirect" , guard ) ;
216+ guest . on ( "will-frame-navigate" , ( details ) => guard ( details , details . url ) ) ;
217+ } ) ;
218+ }
219+
123220function setupCrashLogging ( window : BrowserWindow ) : void {
124221 window . webContents . on ( "render-process-gone" , ( _event , details ) => {
125222 log . error ( "Renderer process gone" , {
@@ -230,6 +327,7 @@ export function createWindow(): void {
230327 webPreferences : {
231328 nodeIntegration : false ,
232329 contextIsolation : true ,
330+ webviewTag : true ,
233331 preload : path . join ( __dirname , "preload.js" ) ,
234332 enableBlinkFeatures : "GetDisplayMedia" ,
235333 partition : "persist:main" ,
@@ -312,6 +410,7 @@ export function createWindow(): void {
312410 } ) ;
313411
314412 setupExternalLinkHandlers ( mainWindow ) ;
413+ setupWebviewHandlers ( mainWindow ) ;
315414 setupEditableContextMenu ( mainWindow ) ;
316415 setupCrashLogging ( mainWindow ) ;
317416 buildApplicationMenu ( ) ;
0 commit comments