Fix insecure HTTP proceed to be one-time unless saved

This commit is contained in:
Lawrence Chen 2026-02-20 15:16:12 -08:00
parent d1bcb17b0d
commit 3ffceb7e8e
3 changed files with 58 additions and 42 deletions

View file

@ -194,27 +194,35 @@ enum BrowserInsecureHTTPSettings {
func browserShouldBlockInsecureHTTPURL(
_ url: URL,
defaults: UserDefaults = .standard,
runtimeAllowedHosts: Set<String> = []
defaults: UserDefaults = .standard
) -> Bool {
browserShouldBlockInsecureHTTPURL(
url,
rawAllowlist: defaults.string(forKey: BrowserInsecureHTTPSettings.allowlistKey),
runtimeAllowedHosts: runtimeAllowedHosts
rawAllowlist: defaults.string(forKey: BrowserInsecureHTTPSettings.allowlistKey)
)
}
func browserShouldBlockInsecureHTTPURL(
_ url: URL,
rawAllowlist: String?,
runtimeAllowedHosts: Set<String> = []
rawAllowlist: String?
) -> Bool {
guard url.scheme?.lowercased() == "http" else { return false }
guard let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else { return true }
if runtimeAllowedHosts.contains(host) {
return !BrowserInsecureHTTPSettings.isHostAllowed(host, rawAllowlist: rawAllowlist)
}
func browserShouldConsumeOneTimeInsecureHTTPBypass(
_ url: URL,
bypassHostOnce: inout String?
) -> Bool {
guard let bypassHost = bypassHostOnce else { return false }
guard url.scheme?.lowercased() == "http",
let host = BrowserInsecureHTTPSettings.normalizeHost(url.host ?? "") else {
return false
}
return !BrowserInsecureHTTPSettings.isHostAllowed(host, rawAllowlist: rawAllowlist)
guard host == bypassHost else { return false }
bypassHostOnce = nil
return true
}
func browserShouldPersistInsecureHTTPAllowlistSelection(
@ -225,23 +233,6 @@ func browserShouldPersistInsecureHTTPAllowlistSelection(
return response == .alertFirstButtonReturn || response == .alertSecondButtonReturn
}
@MainActor
enum BrowserInsecureHTTPRuntimeAllowlist {
private static var hosts = Set<String>()
static func contains(_ host: String) -> Bool {
hosts.contains(host)
}
static func allow(_ host: String) {
hosts.insert(host)
}
static func snapshot() -> Set<String> {
hosts
}
}
enum BrowserUserAgentSettings {
// Force a Safari UA. Some WebKit builds return a minimal UA without Version/Safari tokens,
// and some installs may have legacy Chrome UA overrides. Both can cause Google to serve
@ -1009,6 +1000,7 @@ final class BrowserPanel: Panel, ObservableObject {
private let minPageZoom: CGFloat = 0.25
private let maxPageZoom: CGFloat = 5.0
private let pageZoomStep: CGFloat = 0.1
private var insecureHTTPBypassHostOnce: String?
var displayTitle: String {
if !pageTitle.isEmpty {
@ -1028,9 +1020,10 @@ final class BrowserPanel: Panel, ObservableObject {
false
}
init(workspaceId: UUID, initialURL: URL? = nil) {
init(workspaceId: UUID, initialURL: URL? = nil, bypassInsecureHTTPHostOnce: String? = nil) {
self.id = UUID()
self.workspaceId = workspaceId
self.insecureHTTPBypassHostOnce = BrowserInsecureHTTPSettings.normalizeHost(bypassInsecureHTTPHostOnce ?? "")
// Configure web view
let config = WKWebViewConfiguration()
@ -1432,10 +1425,10 @@ final class BrowserPanel: Panel, ObservableObject {
}
private func shouldBlockInsecureHTTPNavigation(to url: URL) -> Bool {
browserShouldBlockInsecureHTTPURL(
url,
runtimeAllowedHosts: BrowserInsecureHTTPRuntimeAllowlist.snapshot()
)
if browserShouldConsumeOneTimeInsecureHTTPBypass(url, bypassHostOnce: &insecureHTTPBypassHostOnce) {
return false
}
return browserShouldBlockInsecureHTTPURL(url)
}
private func requestNavigation(_ url: URL, intent: BrowserInsecureHTTPNavigationIntent) {
@ -1483,12 +1476,12 @@ final class BrowserPanel: Panel, ObservableObject {
case .alertFirstButtonReturn:
NSWorkspace.shared.open(url)
case .alertSecondButtonReturn:
BrowserInsecureHTTPRuntimeAllowlist.allow(host)
switch intent {
case .currentTab:
insecureHTTPBypassHostOnce = host
navigateWithoutInsecureHTTPPrompt(to: url, recordTypedNavigation: recordTypedNavigation)
case .newTab:
openLinkInNewTab(url: url)
openLinkInNewTab(url: url, bypassInsecureHTTPHostOnce: host)
}
default:
return
@ -1545,11 +1538,16 @@ extension BrowserPanel {
}
/// Open a link in a new browser surface in the same pane
func openLinkInNewTab(url: URL) {
func openLinkInNewTab(url: URL, bypassInsecureHTTPHostOnce: String? = nil) {
guard let tabManager = AppDelegate.shared?.tabManager,
let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }),
let paneId = workspace.paneId(forPanelId: id) else { return }
workspace.newBrowserSurface(inPane: paneId, url: url, focus: true)
workspace.newBrowserSurface(
inPane: paneId,
url: url,
focus: true,
bypassInsecureHTTPHostOnce: bypassInsecureHTTPHostOnce
)
}
/// Reload the current page

View file

@ -571,11 +571,16 @@ final class Workspace: Identifiable, ObservableObject {
inPane paneId: PaneID,
url: URL? = nil,
focus: Bool? = nil,
insertAtEnd: Bool = false
insertAtEnd: Bool = false,
bypassInsecureHTTPHostOnce: String? = nil
) -> BrowserPanel? {
let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId)
let browserPanel = BrowserPanel(workspaceId: id, initialURL: url)
let browserPanel = BrowserPanel(
workspaceId: id,
initialURL: url,
bypassInsecureHTTPHostOnce: bypassInsecureHTTPHostOnce
)
panels[browserPanel.id] = browserPanel
guard let newTabId = bonsplitController.createTab(

View file

@ -130,22 +130,35 @@ final class BrowserInsecureHTTPSettingsTests: XCTestCase {
XCTAssertFalse(BrowserInsecureHTTPSettings.isHostAllowed("example.net", rawAllowlist: raw))
}
func testBlockDecisionUsesAllowlistAndRuntimeProceedCache() throws {
func testBlockDecisionUsesAllowlistAndSchemeRules() throws {
let localURL = try XCTUnwrap(URL(string: "http://foo.localtest.me:3000"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(localURL, rawAllowlist: nil))
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(
insecureURL,
rawAllowlist: nil,
runtimeAllowedHosts: ["neverssl.com"]
))
let httpsURL = try XCTUnwrap(URL(string: "https://neverssl.com"))
XCTAssertFalse(browserShouldBlockInsecureHTTPURL(httpsURL, rawAllowlist: nil))
}
func testOneTimeBypassIsConsumedAfterFirstNavigation() throws {
let insecureURL = try XCTUnwrap(URL(string: "http://neverssl.com"))
var bypassHostOnce: String? = "neverssl.com"
XCTAssertTrue(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertNil(bypassHostOnce)
// Subsequent visits should prompt again unless host was saved.
XCTAssertFalse(browserShouldConsumeOneTimeInsecureHTTPBypass(
insecureURL,
bypassHostOnce: &bypassHostOnce
))
XCTAssertTrue(browserShouldBlockInsecureHTTPURL(insecureURL, rawAllowlist: nil))
}
func testAddAllowedHostPersistsToDefaultsAndUnblocksHTTP() throws {
let suiteName = "BrowserInsecureHTTPSettingsTests.Persist.\(UUID().uuidString)"
guard let defaults = UserDefaults(suiteName: suiteName) else {