Files
iOS/Tests/App/WebView/SafeScriptMessageHandlerTests.swift
Bruno Pantaleão Gonçalves d77040fa07 Normalize ipv6 URL for host comparison in WKSecurityOrigin (#4602)
<!-- Thank you for submitting a Pull Request and helping to improve Home
Assistant. Please complete the following sections to help the processing
and review of your changes. Please do not delete anything from this
template. -->

## Summary
<!-- Provide a brief summary of the changes you have made and most
importantly what they aim to achieve -->
The issue is the new WebView bridge origin check. It compares configured
URL hosts with WKSecurityOrigin.host and IPv6 can appear as fd00::abcd
in one API and [fd00::abcd] in another, so the bridge rejects valid
frontend messages. That blocks external auth and leaves the frontend
loading, while widgets still work.

This PR normalizes the url
## Screenshots
<!-- If this is a user-facing change not in the frontend, please include
screenshots in light and dark mode. -->

## Link to pull request in Documentation repository
<!-- Pull requests that add, change or remove functionality must have a
corresponding pull request in the Companion App Documentation repository
(https://github.com/home-assistant/companion.home-assistant). Please add
the number of this pull request after the "#" -->
Documentation: home-assistant/companion.home-assistant#

## Any other notes
<!-- If there is any other information of note, like if this Pull
Request is part of a bigger change, please include it here. -->
2026-05-04 11:17:51 +02:00

109 lines
4.0 KiB
Swift

@testable import HomeAssistant
import Shared
import Testing
import WebKit
struct SafeScriptMessageHandlerTests {
@Test func allowsMainFrameMessageFromConfiguredServerOrigin() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "https", host: "external.example.com", port: 443))
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "http", host: "internal.example.com", port: 80))
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "https", host: "ui.nabu.casa", port: 443))
}
@Test func allowsMainFrameMessageWhenImplicitPortsAreReportedAsZero() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "https", host: "external.example.com", port: 0))
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "http", host: "internal.example.com", port: 0))
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "https", host: "ui.nabu.casa", port: 0))
}
@Test func allowsMainFrameMessageFromBracketedIPv6Host() {
let handler = SafeScriptMessageHandler(
server: server(internalURL: URL(string: "http://[fd00::abcd]:8123")!),
delegate: NoOpScriptMessageHandler()
)
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "http", host: "[fd00::abcd]", port: 8123))
#expect(handler.shouldAllowMessage(isMainFrame: true, scheme: "http", host: "fd00::abcd", port: 8123))
}
@Test func rejectsMessageFromOriginOutsideConfiguredServerOrigins() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)
#expect(!handler.shouldAllowMessage(isMainFrame: true, scheme: "https", host: "evil.example.com", port: 443))
#expect(!handler.shouldAllowMessage(
isMainFrame: true,
scheme: "https",
host: "external.example.com",
port: 8123
))
#expect(!handler.shouldAllowMessage(isMainFrame: true, scheme: "http", host: "external.example.com", port: 443))
}
@Test func rejectsIframeMessageEvenWhenHostIsAllowed() {
ServerFixture.reset()
let handler = SafeScriptMessageHandler(
server: ServerFixture.withRemoteConnection,
delegate: NoOpScriptMessageHandler()
)
#expect(!handler.shouldAllowMessage(
isMainFrame: false,
scheme: "https",
host: "external.example.com",
port: 443
))
}
}
private func server(internalURL: URL) -> Server {
var info = ServerInfo(
name: "IPv6 Server",
connection: .init(
externalURL: nil,
internalURL: internalURL,
cloudhookURL: nil,
remoteUIURL: nil,
webhookID: "webhook-id",
webhookSecret: nil,
internalSSIDs: nil,
internalHardwareAddresses: nil,
isLocalPushEnabled: false,
securityExceptions: .init(exceptions: []),
connectionAccessSecurityLevel: .undefined
),
token: .init(
accessToken: "access-token",
refreshToken: "refresh-token",
expiration: Date()
),
version: "2026.4.1"
)
return Server(identifier: "ipv6", getter: {
info
}, setter: { newInfo in
info = newInfo
return true
})
}
private final class NoOpScriptMessageHandler: NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {}
}