mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
Security/macos: enforce wss for non-loopback direct gateway
This commit is contained in:
committed by
Peter Steinberger
parent
8942ac04a8
commit
617e38cec0
@@ -480,8 +480,7 @@ final class AppState {
|
||||
remote.removeValue(forKey: "url")
|
||||
remoteChanged = true
|
||||
}
|
||||
} else {
|
||||
let normalizedUrl = GatewayRemoteConfig.normalizeGatewayUrlString(trimmedUrl) ?? trimmedUrl
|
||||
} else if let normalizedUrl = GatewayRemoteConfig.normalizeGatewayUrlString(trimmedUrl) {
|
||||
if (remote["url"] as? String) != normalizedUrl {
|
||||
remote["url"] = normalizedUrl
|
||||
remoteChanged = true
|
||||
|
||||
@@ -42,7 +42,8 @@ enum GatewayDiscoveryHelpers {
|
||||
guard let endpoint = self.serviceEndpoint(serviceHost: serviceHost, servicePort: servicePort) else {
|
||||
return nil
|
||||
}
|
||||
let scheme = endpoint.port == 443 ? "wss" : "ws"
|
||||
// Security: for non-loopback hosts, force TLS to avoid plaintext credential/session leakage.
|
||||
let scheme = self.isLoopbackHost(endpoint.host) ? "ws" : "wss"
|
||||
let portSuffix = endpoint.port == 443 ? "" : ":\(endpoint.port)"
|
||||
return "\(scheme)://\(endpoint.host)\(portSuffix)"
|
||||
}
|
||||
@@ -50,4 +51,16 @@ enum GatewayDiscoveryHelpers {
|
||||
private static func trimmed(_ value: String?) -> String? {
|
||||
value?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
|
||||
private static func isLoopbackHost(_ rawHost: String) -> Bool {
|
||||
let host = rawHost.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
||||
guard !host.isEmpty else { return false }
|
||||
if host == "localhost" || host == "::1" || host == "0:0:0:0:0:0:0:1" {
|
||||
return true
|
||||
}
|
||||
if host.hasPrefix("::ffff:127.") {
|
||||
return true
|
||||
}
|
||||
return host.hasPrefix("127.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +303,9 @@ struct GeneralSettings: View {
|
||||
.disabled(self.remoteStatus == .checking || self.state.remoteUrl
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||
}
|
||||
Text("Direct mode requires a ws:// or wss:// URL (Tailscale Serve uses wss://<magicdns>).")
|
||||
Text(
|
||||
"Direct mode requires wss:// for remote hosts. ws:// is only allowed for localhost/127.0.0.1."
|
||||
)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, self.remoteLabelWidth + 10)
|
||||
@@ -546,7 +548,9 @@ extension GeneralSettings {
|
||||
return
|
||||
}
|
||||
guard Self.isValidWsUrl(trimmedUrl) else {
|
||||
self.remoteStatus = .failed("Gateway URL must start with ws:// or wss://")
|
||||
self.remoteStatus = .failed(
|
||||
"Gateway URL must use wss:// for remote hosts (ws:// only for localhost)"
|
||||
)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@@ -603,11 +607,7 @@ extension GeneralSettings {
|
||||
}
|
||||
|
||||
private static func isValidWsUrl(_ raw: String) -> Bool {
|
||||
guard let url = URL(string: raw.trimmingCharacters(in: .whitespacesAndNewlines)) else { return false }
|
||||
let scheme = url.scheme?.lowercased() ?? ""
|
||||
guard scheme == "ws" || scheme == "wss" else { return false }
|
||||
let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
||||
return !host.isEmpty
|
||||
GatewayRemoteConfig.normalizeGatewayUrl(raw) != nil
|
||||
}
|
||||
|
||||
private static func sshCheckCommand(target: String, identity: String) -> [String]? {
|
||||
|
||||
@@ -62,7 +62,12 @@ struct GatewayDiscoveryHelpersTests {
|
||||
let wsGateway = self.makeGateway(
|
||||
serviceHost: "resolved.example.ts.net",
|
||||
servicePort: 18789)
|
||||
#expect(GatewayDiscoveryHelpers.directUrl(for: wsGateway) == "ws://resolved.example.ts.net:18789")
|
||||
#expect(GatewayDiscoveryHelpers.directUrl(for: wsGateway) == "wss://resolved.example.ts.net:18789")
|
||||
|
||||
let localGateway = self.makeGateway(
|
||||
serviceHost: "127.0.0.1",
|
||||
servicePort: 18789)
|
||||
#expect(GatewayDiscoveryHelpers.directUrl(for: localGateway) == "ws://127.0.0.1:18789")
|
||||
}
|
||||
|
||||
@Test func directUrlRejectsTxtOnlyFallback() {
|
||||
|
||||
@@ -225,7 +225,7 @@ import Testing
|
||||
}
|
||||
|
||||
@Test func normalizeGatewayUrlRejectsNonLoopbackWs() {
|
||||
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://gateway")
|
||||
let url = GatewayRemoteConfig.normalizeGatewayUrl("ws://gateway.example:18789")
|
||||
#expect(url == nil)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user