Parcourir la Source

Restrict embedded Google browsing to sign-in only.

Block Gmail, Help, and other Google apps in the Indeed web view while allowing OAuth, and inject prompt=select_account so users can choose which Google account to use.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 il y a 2 semaines
Parent
commit
f9d2da77ac
1 fichiers modifiés avec 151 ajouts et 2 suppressions
  1. 151 2
      App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

+ 151 - 2
App for Indeed/Controllers/IndeedJobBrowserWindowController.swift

@@ -185,14 +185,163 @@ final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelega
185 185
         updateNavigationButtons()
186 186
     }
187 187
 
188
+    func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
189
+        guard IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
190
+            decisionHandler(.cancel)
191
+            return
192
+        }
193
+
194
+        let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
195
+        if isMainFrame,
196
+           let url = navigationAction.request.url,
197
+           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
198
+            let pickerURL = IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url)
199
+            if pickerURL != url {
200
+                webView.load(URLRequest(url: pickerURL))
201
+                decisionHandler(.cancel)
202
+                return
203
+            }
204
+        }
205
+
206
+        decisionHandler(.allow)
207
+    }
208
+
188 209
     /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
189 210
     func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
190
-        if navigationAction.targetFrame == nil {
191
-            webView.load(navigationAction.request)
211
+        guard navigationAction.targetFrame == nil,
212
+              IndeedWebBrowsingPolicy.allows(navigationAction: navigationAction) else {
213
+            return nil
192 214
         }
215
+        var request = navigationAction.request
216
+        if let url = request.url,
217
+           IndeedWebBrowsingPolicy.shouldForceGoogleAccountPicker(for: url) {
218
+            request = URLRequest(url: IndeedWebBrowsingPolicy.urlForcingGoogleAccountPicker(url))
219
+        }
220
+        webView.load(request)
193 221
         return nil
194 222
     }
195 223
 
196 224
     /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
197 225
     private static let desktopSafariLikeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
198 226
 }
227
+
228
+// MARK: - Google sign-in only (no Gmail, Help hub, Search, etc.)
229
+
230
+/// Limits embedded browsing on Google-owned hosts to OAuth / sign-in, while leaving Indeed and third-party apply sites unrestricted.
231
+enum IndeedWebBrowsingPolicy {
232
+    static func allows(navigationAction: WKNavigationAction) -> Bool {
233
+        guard let url = navigationAction.request.url else { return true }
234
+        let isMainFrame = navigationAction.targetFrame?.isMainFrame ?? true
235
+        if isMainFrame {
236
+            return allowsMainFrameNavigation(to: url)
237
+        }
238
+        return allowsSubframeNavigation(to: url)
239
+    }
240
+
241
+    static func allowsMainFrameNavigation(to url: URL) -> Bool {
242
+        guard let scheme = url.scheme?.lowercased() else { return false }
243
+        if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
244
+        guard scheme == "http" || scheme == "https" else { return false }
245
+        guard let host = url.host?.lowercased() else { return false }
246
+
247
+        if !isGooglePropertyHost(host) {
248
+            return true
249
+        }
250
+        return isAllowedGoogleSignInHost(host, path: url.path)
251
+    }
252
+
253
+    static func allowsSubframeNavigation(to url: URL) -> Bool {
254
+        guard let scheme = url.scheme?.lowercased() else { return false }
255
+        if scheme == "about" || scheme == "blob" || scheme == "data" { return true }
256
+        guard scheme == "http" || scheme == "https" else { return false }
257
+        guard let host = url.host?.lowercased() else { return false }
258
+
259
+        if !isGooglePropertyHost(host) {
260
+            return true
261
+        }
262
+        if isAllowedGoogleSignInHost(host, path: url.path) {
263
+            return true
264
+        }
265
+        let path = url.path.lowercased()
266
+        if host == "www.google.com", path.contains("/recaptcha") {
267
+            return true
268
+        }
269
+        if host == "gstatic.com" || host.hasSuffix(".gstatic.com") {
270
+            return true
271
+        }
272
+        return false
273
+    }
274
+
275
+    private static func isGooglePropertyHost(_ host: String) -> Bool {
276
+        if host == "google.com" || host.hasSuffix(".google.com") { return true }
277
+        if host == "gmail.com" || host.hasSuffix(".gmail.com") { return true }
278
+        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") { return true }
279
+        if host == "gstatic.com" || host.hasSuffix(".gstatic.com") { return true }
280
+        if host == "youtube.com" || host.hasSuffix(".youtube.com") { return true }
281
+        if host == "blogger.com" || host.hasSuffix(".blogger.com") { return true }
282
+        if host == "withgoogle.com" || host.hasSuffix(".withgoogle.com") { return true }
283
+        return false
284
+    }
285
+
286
+    /// True for Google OAuth / sign-in navigations where we inject `prompt=select_account`.
287
+    static func shouldForceGoogleAccountPicker(for url: URL) -> Bool {
288
+        guard let host = url.host?.lowercased(),
289
+              host == "accounts.google.com" || host.hasPrefix("accounts.google.") else {
290
+            return false
291
+        }
292
+        let path = url.path.lowercased()
293
+        if path.contains("oauth")
294
+            || path.contains("accountchooser")
295
+            || path.contains("/gsi/")
296
+            || path.hasPrefix("/signin/") {
297
+            return true
298
+        }
299
+        let query = url.query?.lowercased() ?? ""
300
+        return query.contains("client_id=")
301
+    }
302
+
303
+    /// Rewrites Google sign-in URLs so the account chooser is always shown (not silent reuse of the last account).
304
+    static func urlForcingGoogleAccountPicker(_ url: URL) -> URL {
305
+        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url }
306
+        var items = components.queryItems ?? []
307
+
308
+        items.removeAll { $0.name.compare("login_hint", options: .caseInsensitive) == .orderedSame }
309
+        items.removeAll { $0.name.compare("hint", options: .caseInsensitive) == .orderedSame }
310
+
311
+        if let promptIndex = items.firstIndex(where: { $0.name.compare("prompt", options: .caseInsensitive) == .orderedSame }) {
312
+            let existing = items[promptIndex].value ?? ""
313
+            if !existing.lowercased().contains("select_account") {
314
+                let merged = existing.isEmpty ? "select_account" : "\(existing) select_account"
315
+                items[promptIndex] = URLQueryItem(name: "prompt", value: merged)
316
+            }
317
+        } else {
318
+            items.append(URLQueryItem(name: "prompt", value: "select_account"))
319
+        }
320
+
321
+        components.queryItems = items.isEmpty ? nil : items
322
+        return components.url ?? url
323
+    }
324
+
325
+    /// Hosts used during “Sign in with Google” — not Help Center, Gmail, Drive, Search, or the apps launcher.
326
+    private static func isAllowedGoogleSignInHost(_ host: String, path: String) -> Bool {
327
+        if host == "accounts.google.com" || host.hasPrefix("accounts.google.") {
328
+            return true
329
+        }
330
+        if host == "signin.google.com" {
331
+            return true
332
+        }
333
+        if host == "oauth.googleusercontent.com" {
334
+            return true
335
+        }
336
+        if host == "googleusercontent.com" || host.hasSuffix(".googleusercontent.com") {
337
+            return true
338
+        }
339
+        if host == "policies.google.com" || host == "privacy.google.com" {
340
+            return true
341
+        }
342
+        if host == "apis.google.com" {
343
+            return true
344
+        }
345
+        return false
346
+    }
347
+}