CVE-2025-46282
Overview
Impact:
Description: An app may be able to access sensitive user data
Researchers: Wojciech Regula of SecuRing (wojciechregula.blog)
Key insight. Media-capture permission grants were cached and reused by origin without regard to whether the page came from a web archive, so a crafted archive could inherit a prior grant; forcing archive-loaded pages to always prompt (and never cache) closes the bypass.
Patch Details
diff --git a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
index 286af00b7156..27279c6e0c33 100644
--- a/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
+++ b/Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp
@@ -267,7 +267,12 @@ void UserMediaPermissionRequestManagerProxy::denyRequest(UserMediaPermissionRequ
ALWAYS_LOG(LOGIDENTIFIER, request.userMediaID() ? request.userMediaID()->toUInt64() : 0, ", reason: ", reason);
- if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied)
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+
+ if (reason == UserMediaPermissionRequestProxy::UserMediaAccessDenialReason::PermissionDenied && shouldCacheResult)
m_deniedRequests.append(DeniedRequest { request.mainFrameID(), request.userMediaDocumentSecurityOrigin(), request.topLevelDocumentSecurityOrigin(), request.requiresAudioCapture(), request.requiresVideoCapture(), request.requiresDisplayCapture() });
if (auto callback = request.decisionCompletionHandler()) {
@@ -299,7 +304,12 @@ void UserMediaPermissionRequestManagerProxy::grantRequest(UserMediaPermissionReq
page->willStartCapture(request, [callback = WTFMove(callback)]() mutable {
callback(true);
});
- m_grantedRequests.append(request);
+ bool shouldCacheResult = true;
+#if ENABLE(WEB_ARCHIVE)
+ shouldCacheResult = !page->didLoadWebArchive();
+#endif
+ if (shouldCacheResult)
+ m_grantedRequests.append(request);
if (request.requiresAudioCapture())
m_grantedAudioFrames.add(request.frameID());
if (request.requiresVideoCapture())
@@ -442,6 +452,11 @@ bool UserMediaPermissionRequestManagerProxy::hasGrantedRequest(std::optional<Fra
if (!isUserGesturePriviledged && inactiveMediaCaptureStreamDuration().minutes() > page->protectedPreferences()->inactiveMediaCaptureStreamRepromptWithoutUserGestureIntervalInMinutes())
return false;
+#if ENABLE(WEB_ARCHIVE)
+ if (page->didLoadWebArchive())
+ return false;
+#endif
+
bool checkForAudio = needsAudio;
bool checkForVideo = needsVideo;
for (Ref grantedRequest : m_grantedRequests) {
diff --git a/Source/WebKit/UIProcess/WebPageProxy.cpp b/Source/WebKit/UIProcess/WebPageProxy.cpp
index f07d5b338236..7ec2f1fa8245 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.cpp
+++ b/Source/WebKit/UIProcess/WebPageProxy.cpp
@@ -12814,6 +12814,48 @@ void WebPageProxy::revokeGeolocationAuthorizationToken(const String& authorizati
internals().protectedGeolocationPermissionRequestManager()->revokeAuthorizationToken(authorizationToken);
}
+#if ENABLE(WEB_ARCHIVE)
+bool WebPageProxy::shouldAlwaysPromptForPermission(PermissionName permissionName) const
+{
+ if (!didLoadWebArchive())
+ return false;
+
+ switch (permissionName) {
+ // All cached permissions that are supported by platforms that support
+ // WEB_ARCHIVE should always prompt.
+ case PermissionName::Camera:
+ case PermissionName::Geolocation:
+ case PermissionName::Microphone:
+ break;
+
+ // Notifications are not available in ephemeral sessions.
+ case PermissionName::Notifications:
+ case PermissionName::Push:
+
+ // Orientation and motion data are not emitted.
+ case PermissionName::Accelerometer:
+ case PermissionName::Gyroscope:
+ case PermissionName::Magnetometer:
+
+ // These are not persistent permissions.
+ case PermissionName::BackgroundFetch:
+ case PermissionName::DisplayCapture:
+ case PermissionName::ScreenWakeLock:
+ case PermissionName::SpeakerSelection:
+ case PermissionName::StorageAccess:
+
+ // Not supported.
+ case PermissionName::Bluetooth:
+ case PermissionName::Midi:
+ case PermissionName::Nfc:
+ ASSERT_NOT_REACHED();
+ break;
+ }
+
+ return true;
+}
+#endif
+
void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const PermissionDescriptor& descriptor, CompletionHandler<void(std::optional<PermissionState>)>&& completionHandler)
{
bool canAPISucceed = true;
@@ -12822,8 +12864,8 @@ void WebPageProxy::queryPermission(const ClientOrigin& clientOrigin, const Permi
String name;
#if ENABLE(WEB_ARCHIVE)
- if (didLoadWebArchive()) {
- completionHandler(PermissionState::Denied);
+ if (shouldAlwaysPromptForPermission(descriptor.name)) {
+ completionHandler(PermissionState::Prompt);
return;
}
#endif
diff --git a/Source/WebKit/UIProcess/WebPageProxy.h b/Source/WebKit/UIProcess/WebPageProxy.h
index 490eab241948..20770b71e251 100644
--- a/Source/WebKit/UIProcess/WebPageProxy.h
+++ b/Source/WebKit/UIProcess/WebPageProxy.h
@@ -3107,6 +3107,10 @@ private:
void requestNotificationPermission(const String& originString, CompletionHandler<void(bool allowed)>&&);
+#if ENABLE(WEB_ARCHIVE)
+ bool shouldAlwaysPromptForPermission(WebCore::PermissionName) const;
+#endif
+
void didChangeContentSize(const WebCore::IntSize&);
void didChangeIntrinsicContentSize(const WebCore::IntSize&);
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 51d51c95fb59..cb89c353447d 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -763,6 +763,7 @@
A17991881E1C994E00A505ED /* SharedBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A17991861E1C994E00A505ED /* SharedBuffer.mm */; };
A179918B1E1CA24100A505ED /* SharedBufferTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A17991891E1CA24100A505ED /* SharedBufferTest.cpp */; };
A17C46592C98E4A80023F3C7 /* download.example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414A /* download.example.webarchive */; };
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414B /* example.webarchive */; };
A17C465A2C98E4BB0023F3C7 /* active-touch-events.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4A7CE792662D83E00228685 /* active-touch-events.html */; };
A17C465B2C98E4BB0023F3C7 /* animated-red-green-blue-repeat-infinite.gif in Copy Resources */ = {isa = PBXBuildFile; fileRef = 48354DD129F08036003E566D /* animated-red-green-blue-repeat-infinite.gif */; };
A17C465C2C98E4BB0023F3C7 /* composited.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F16BED72304A1D100B4A167 /* composited.html */; };
@@ -2002,6 +2003,7 @@
A17C475F2C98E5C20023F3C7 /* enormous.svg in Copy Resources */,
A17C46AF2C98E54B0023F3C7 /* enumerateMediaDevices.html in Copy Resources */,
A17C47602C98E5C20023F3C7 /* event.ics in Copy Resources */,
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */,
A17C46B02C98E54B0023F3C7 /* execCopy.html in Copy Resources */,
A17C47612C98E5C20023F3C7 /* exif-orientation-8-llo.jpg in Copy Resources */,
A17C47622C98E5C20023F3C7 /* ExitFullscreenOnEnterPiP.html in Copy Resources */,
@@ -4104,6 +4106,7 @@
CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
D04CF93E285C77C9005D6337 /* MachSendRight.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MachSendRight.cpp; sourceTree = "<group>"; };
D20C03AE2B704A26004C414A /* download.example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = download.example.webarchive; path = Tests/WebKitCocoa/download.example.webarchive; sourceTree = "<group>"; };
+ D20C03AE2B704A26004C414B /* example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = example.webarchive; path = Tests/WebKitCocoa/example.webarchive; sourceTree = "<group>"; };
D2CC7CF22D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = TestCocoaImageAndCocoaColor.h; path = cocoa/TestCocoaImageAndCocoaColor.h; sourceTree = "<group>"; };
D2CC7CF32D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = TestCocoaImageAndCocoaColor.mm; path = cocoa/TestCocoaImageAndCocoaColor.mm; sourceTree = "<group>"; };
D2D9A6BD2D96B539004A2C92 /* icon-with-subresource.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "icon-with-subresource.svg"; sourceTree = "<group>"; };
@@ -4594,6 +4597,7 @@
isa = PBXGroup;
children = (
D20C03AE2B704A26004C414A /* download.example.webarchive */,
+ D20C03AE2B704A26004C414B /* example.webarchive */,
3AA7856029B6C8AA00C4374A /* TestWebKitAPIPrefix.h */,
DDF3A82928930475005920CF /* gtest.xcodeproj */,
49AEEF682407276F00C87E4C /* Info.plist */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 8f258d6448ea..24fe09c7016e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2138,6 +2138,87 @@ TEST(WebKit, GetUserMediaWithWebThread)
}
#endif // PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
+#if ENABLE(WEB_ARCHIVE)
+TEST(GetUserMedia, OriginAndWebArchivePermission)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { "body"_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ initializeMediaCaptureConfiguration(configuration.get());
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+
+ RetainPtr navDelegate = adoptNS([TestNavigationDelegate new]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ [navDelegate allowAnyTLSCertificate];
+ [webView setUIDelegate:delegate.get()];
+ [webView setNavigationDelegate:navDelegate.get()];
+
+ [delegate setAudioDecision:WKPermissionDecisionGrant];
+
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-46282 |
| Bugzilla | 295941 |
| Component | WebKit |
| Bug Class | Bypass |
| Severity | high |
| Commit | 50b0e0bcc62b2450… |
| Advisory | Apple Advisory |
Terminology
- Web Archive (.webarchive) — A saved snapshot of a page (content under some origin) that can be opened later; an attacker can craft one presenting a chosen origin.
- Permission caching — WebKit remembers granted/denied capture permissions per origin to avoid re-prompting; reusing that cache for archive content is unsafe.
- didLoadWebArchive — A page flag indicating the current content came from a web archive, used to force prompting.
Root Cause Analysis
This fixes a permission bypass where content loaded from a saved Web Archive could silently reuse cached camera/microphone/geolocation grants. WebKit caches media-capture permission decisions per origin so a site does not re-prompt: grantRequest appends to m_grantedRequests, denyRequest appends to m_deniedRequests, and hasGrantedRequest consults m_grantedRequests to auto-approve a matching origin without a prompt. A .webarchive file bundles content under an arbitrary origin; if a user opens a malicious web archive whose origin matches a site the user previously granted capture to (or if the archive can otherwise ride the cache), the archive’s content could obtain camera/microphone/geolocation access WITHOUT a fresh user prompt — accessing sensitive user data. The fix makes web-archive-loaded pages never populate or reuse the permission cache: grantRequest and denyRequest set shouldCacheResult = !page->didLoadWebArchive() and skip caching when a web archive was loaded; hasGrantedRequest returns false immediately when page->didLoadWebArchive(); and a new WebPageProxy::shouldAlwaysPromptForPermission returns true for Camera/Microphone/Geolocation when the page loaded a web archive. The restored invariant is that a page loaded from a web archive always prompts for capture/geolocation permissions and cannot inherit or seed cached grants. The impact — reachable by getting a user to open a crafted .webarchive — is unauthorized access to sensitive sensors, not memory corruption.
Attack Path
1. Craft a malicious web archive
Build a .webarchive presenting content under an origin the user has previously granted (or otherwise able to hit the permission cache).
2. Get the user to open it
The user opens the web archive, loading its content into WebKit as a web-archive page.
3. Reuse the cached grant
Pre-patch, hasGrantedRequest matches the cached grant for the origin and auto-approves capture without prompting.
4. Access sensitive data
The archive content activates camera/microphone/geolocation and exfiltrates sensitive user data without consent.
Impact Assessment
An access-control / privacy bypass in the UI process with no memory corruption: opening a crafted web archive could grant it camera, microphone, or geolocation access by reusing cached permissions, exposing sensitive user data. It requires the user to open a malicious .webarchive, but the outcome — silent sensor access — is serious (advisory: access sensitive user data).
Audit Directions
Other cached permissions vs web archive
Audit permission/state caches (notifications, storage access, geolocation tokens) for reuse on didLoadWebArchive() pages without an always-prompt guard.
Origin trust for archives
Grep UIProcess for permission/grant lookups keyed only on security origin that do not distinguish web-archive-loaded content.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
UserMediaPermissionRequestManagerProxy::grantRequest / denyRequest |
Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp |
modified | Skips caching the grant/deny (shouldCacheResult = !page->didLoadWebArchive()) when the page was loaded from a web archive. |
UserMediaPermissionRequestManagerProxy::hasGrantedRequest |
Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cpp |
modified | Returns false (forcing a prompt) when page->didLoadWebArchive(), so archive pages never auto-approve from cache. |
WebPageProxy::shouldAlwaysPromptForPermission |
Source/WebKit/UIProcess/WebPageProxy.cpp |
added | Returns true for Camera/Microphone/Geolocation when a web archive was loaded, ensuring those permissions always prompt. |
Files Changed
Source Files
Source/WebKit/UIProcess/UserMediaPermissionRequestManagerProxy.cppSource/WebKit/UIProcess/WebPageProxy.cppSource/WebKit/UIProcess/WebPageProxy.h
Test Files
Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxprojTools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mmTools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mmTools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mmTools/TestWebKitAPI/Tests/WebKitCocoa/example.webarchive
Regression Test
The fix ships this proof-of-concept / regression test:
diff --git a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
index 51d51c95fb59..cb89c353447d 100644
--- a/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
+++ b/Tools/TestWebKitAPI/TestWebKitAPI.xcodeproj/project.pbxproj
@@ -763,6 +763,7 @@
A17991881E1C994E00A505ED /* SharedBuffer.mm in Sources */ = {isa = PBXBuildFile; fileRef = A17991861E1C994E00A505ED /* SharedBuffer.mm */; };
A179918B1E1CA24100A505ED /* SharedBufferTest.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A17991891E1CA24100A505ED /* SharedBufferTest.cpp */; };
A17C46592C98E4A80023F3C7 /* download.example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414A /* download.example.webarchive */; };
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */ = {isa = PBXBuildFile; fileRef = D20C03AE2B704A26004C414B /* example.webarchive */; };
A17C465A2C98E4BB0023F3C7 /* active-touch-events.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = F4A7CE792662D83E00228685 /* active-touch-events.html */; };
A17C465B2C98E4BB0023F3C7 /* animated-red-green-blue-repeat-infinite.gif in Copy Resources */ = {isa = PBXBuildFile; fileRef = 48354DD129F08036003E566D /* animated-red-green-blue-repeat-infinite.gif */; };
A17C465C2C98E4BB0023F3C7 /* composited.html in Copy Resources */ = {isa = PBXBuildFile; fileRef = 0F16BED72304A1D100B4A167 /* composited.html */; };
@@ -2002,6 +2003,7 @@
A17C475F2C98E5C20023F3C7 /* enormous.svg in Copy Resources */,
A17C46AF2C98E54B0023F3C7 /* enumerateMediaDevices.html in Copy Resources */,
A17C47602C98E5C20023F3C7 /* event.ics in Copy Resources */,
+ A17C465A2C98E4A80023F3C7 /* example.webarchive in Copy Resources */,
A17C46B02C98E54B0023F3C7 /* execCopy.html in Copy Resources */,
A17C47612C98E5C20023F3C7 /* exif-orientation-8-llo.jpg in Copy Resources */,
A17C47622C98E5C20023F3C7 /* ExitFullscreenOnEnterPiP.html in Copy Resources */,
@@ -4104,6 +4106,7 @@
CEDA12402437C9EA00C28A9E /* editable-region-composited-and-non-composited-overlap.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "editable-region-composited-and-non-composited-overlap.html"; path = "ios/editable-region-composited-and-non-composited-overlap.html"; sourceTree = SOURCE_ROOT; };
D04CF93E285C77C9005D6337 /* MachSendRight.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = MachSendRight.cpp; sourceTree = "<group>"; };
D20C03AE2B704A26004C414A /* download.example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = download.example.webarchive; path = Tests/WebKitCocoa/download.example.webarchive; sourceTree = "<group>"; };
+ D20C03AE2B704A26004C414B /* example.webarchive */ = {isa = PBXFileReference; lastKnownFileType = file.bplist; name = example.webarchive; path = Tests/WebKitCocoa/example.webarchive; sourceTree = "<group>"; };
D2CC7CF22D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = TestCocoaImageAndCocoaColor.h; path = cocoa/TestCocoaImageAndCocoaColor.h; sourceTree = "<group>"; };
D2CC7CF32D9A8D6B00011B9E /* TestCocoaImageAndCocoaColor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; name = TestCocoaImageAndCocoaColor.mm; path = cocoa/TestCocoaImageAndCocoaColor.mm; sourceTree = "<group>"; };
D2D9A6BD2D96B539004A2C92 /* icon-with-subresource.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; lineEnding = 0; path = "icon-with-subresource.svg"; sourceTree = "<group>"; };
@@ -4594,6 +4597,7 @@
isa = PBXGroup;
children = (
D20C03AE2B704A26004C414A /* download.example.webarchive */,
+ D20C03AE2B704A26004C414B /* example.webarchive */,
3AA7856029B6C8AA00C4374A /* TestWebKitAPIPrefix.h */,
DDF3A82928930475005920CF /* gtest.xcodeproj */,
49AEEF682407276F00C87E4C /* Info.plist */,
diff --git a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
index 8f258d6448ea..24fe09c7016e 100644
--- a/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKit/GetUserMedia.mm
@@ -2138,6 +2138,87 @@ TEST(WebKit, GetUserMediaWithWebThread)
}
#endif // PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST)
+#if ENABLE(WEB_ARCHIVE)
+TEST(GetUserMedia, OriginAndWebArchivePermission)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { "body"_s } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ initializeMediaCaptureConfiguration(configuration.get());
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 320, 500) configuration:configuration.get()]);
+ auto delegate = adoptNS([[UserMediaCaptureUIDelegate alloc] init]);
+
+ RetainPtr navDelegate = adoptNS([TestNavigationDelegate new]);
+
+ auto observer = adoptNS([[MediaCaptureObserver alloc] init]);
+ [webView addObserver:observer.get() forKeyPath:@"microphoneCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+ [webView addObserver:observer.get() forKeyPath:@"cameraCaptureState" options:NSKeyValueObservingOptionNew context:nil];
+
+ [navDelegate allowAnyTLSCertificate];
+ [webView setUIDelegate:delegate.get()];
+ [webView setNavigationDelegate:navDelegate.get()];
+
+ [delegate setAudioDecision:WKPermissionDecisionGrant];
+
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navDelegate waitForDidFinishNavigation];
+
+ cameraCaptureStateChange = false;
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateNone);
+ [webView evaluateJavaScript:@"navigator.mediaDevices.enumerateDevices().then(() => { return navigator.mediaDevices.getUserMedia({ audio: false, video: true }) });" completionHandler:nil];
+ EXPECT_TRUE(waitUntilCameraState(webView.get(), WKMediaCaptureStateActive));
+ EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateActive);
+
+ [delegate waitUntilPrompted];
+ [delegate resetWasPrompted];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"camera\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"microphone\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"try { await navigator.mediaDevices.getUserMedia({ audio: false, video: true }); return null; } catch (err) { return err.name; }" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"TypeError", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ EXPECT_EQ([delegate numberOfPrompts], 1);
+ [webView removeObserver:observer.get() forKeyPath:@"microphoneCaptureState"];
+ [webView removeObserver:observer.get() forKeyPath:@"cameraCaptureState"];
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(MEDIA_STREAM)
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
index 2b1e3b3cb949..9ca993324891 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/NotificationAPI.mm
@@ -28,6 +28,7 @@
#if ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
#import "DeprecatedGlobalValues.h"
#import "PlatformUtilities.h"
+#import "TestNavigationDelegate.h"
#import "TestWKWebView.h"
#import <WebKit/WKUIDelegatePrivate.h>
#import <wtf/Function.h>
@@ -169,6 +170,40 @@ TEST(Notification, ParallelPermissionRequestsGranted)
runParallelPermissionRequestsTest(ShouldGrantPermission::Yes);
}
+#if ENABLE(WEB_ARCHIVE)
+TEST(Notification, WebArchiveNotificationNotSupported)
+{
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ auto messageHandler = adoptNS([[NotificationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]);
+ RetainPtr uiDelegate = adoptNS([[NotificationPermissionUIDelegate alloc] initWithHandler:[] {
+ return true;
+ }]);
+ [webView setUIDelegate:uiDelegate.get()];
+
+ RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [webView setNavigationDelegate:navigationDelegate.get()];
+ [navigationDelegate allowAnyTLSCertificate];
+
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+
+ [navigationDelegate waitForDidFinishNavigation];
+
+ didReceiveMessage = false;
+ clientPermissionRequestCount = 0;
+ [receivedMessages removeAllObjects];
+
+ [webView evaluateJavaScript:@"Notification.requestPermission((permission) => { webkit.messageHandlers.testHandler.postMessage(permission) });" completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+
+ EXPECT_EQ(clientPermissionRequestCount, 0U);
+ EXPECT_WK_STREQ(@"denied", receivedMessages.get()[0]);
+}
+#endif
+
} // namespace TestWebKitAPI
#endif // ENABLE(NOTIFICATIONS) && !(PLATFORM(IOS) || PLATFORM(VISION))
diff --git a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
index c64d263224b9..384ca0dee99b 100644
--- a/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
+++ b/Tools/TestWebKitAPI/Tests/WebKitCocoa/UIDelegate.mm
@@ -63,8 +63,10 @@
@end
#endif
+#import <WebKit/WKWebsiteDataStorePrivate.h>
#import <WebKit/_WKFeature.h>
#import <WebKit/_WKHitTestResult.h>
+#import <WebKit/_WKWebsiteDataStoreConfiguration.h>
#import <wtf/BlockPtr.h>
#import <wtf/RetainPtr.h>
#import <wtf/Vector.h>
@@ -343,6 +345,115 @@ TEST(WebKit, GeolocationPermissionInIFrame)
EXPECT_TRUE(done);
}
+TEST(WebKit, GeolocationPermissionInIFrameExampleWebArchive)
+{
+ TestWebKitAPI::HTTPServer server({
+ { "/"_s, { mainFrameText } }
+ }, TestWebKitAPI::HTTPServer::Protocol::HttpsProxy);
+
+ auto pool = adoptNS([[WKProcessPool alloc] init]);
+
+ WKGeolocationProviderV1 providerCallback;
+ zeroBytes(providerCallback);
+ providerCallback.base.version = 1;
+ providerCallback.startUpdating = [] (WKGeolocationManagerRef manager, const void*) {
+ WKGeolocationManagerProviderDidChangePosition(manager, adoptWK(WKGeolocationPositionCreate(0, 50.644358, 3.345453, 2.53)).get());
+ };
+ WKGeolocationManagerSetProvider(WKContextGetGeolocationManager((WKContextRef)pool.get()), &providerCallback.base);
+
+ auto storeConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] initNonPersistentConfiguration]);
+ [storeConfiguration setProxyConfiguration:@{
+ (NSString *)kCFStreamPropertyHTTPSProxyHost: @"127.0.0.1",
+ (NSString *)kCFStreamPropertyHTTPSProxyPort: @(server.port())
+ }];
+ auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]);
+
+ auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
+ configuration.get().processPool = pool.get();
+ [configuration setWebsiteDataStore:dataStore.get()];
+
+ auto messageHandler = adoptNS([[GeolocationPermissionMessageHandler alloc] init]);
+ [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"testHandler"];
+
+ auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]);
+
+ auto permissionDelegate = adoptNS([[GeolocationDelegateNew alloc] init]);
+ [webView setUIDelegate:permissionDelegate.get()];
+
+ auto navigationDelegate = adoptNS([TestNavigationDelegate new]);
+ [navigationDelegate allowAnyTLSCertificate];
+
+ webView.get().navigationDelegate = navigationDelegate.get();
+
+ [permissionDelegate setValidationHandler:[&webView](WKSecurityOrigin *origin, WKFrameInfo *frame) {
+ if ([origin.protocol isEqualToString:@"https"]) {
+ EXPECT_WK_STREQ(origin.protocol, @"https");
+ EXPECT_WK_STREQ(origin.host, @"example.com");
+ } else {
+ EXPECT_WK_STREQ(origin.protocol, @"file");
+ EXPECT_WK_STREQ(origin.host, @"");
+ }
+ EXPECT_EQ(origin.port, 0);
+
+ EXPECT_WK_STREQ(frame.securityOrigin.protocol, @"https");
+ EXPECT_WK_STREQ(frame.securityOrigin.host, @"example.com");
+ EXPECT_EQ(frame.securityOrigin.port, 0);
+ EXPECT_TRUE(frame.isMainFrame);
+ EXPECT_TRUE(frame.webView == webView);
+ }];
+
+ NSString *getCurrentPosition = @"navigator.geolocation.getCurrentPosition(() => { webkit.messageHandlers.testHandler.postMessage(\"ok\") }, () => { webkit.messageHandlers.testHandler.postMessage(\"ko\") });";
+
+ done = false;
+ didReceiveMessage = false;
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+ [webView evaluateJavaScript:getCurrentPosition completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+ TestWebKitAPI::Util::run(&done);
+
+#if ENABLE(WEB_ARCHIVE)
+ RetainPtr<NSURL> testURL = [NSBundle.test_resourcesBundle URLForResource:@"example" withExtension:@"webarchive"];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navigationDelegate waitForDidFinishNavigation];
+ __block bool doneEvaluatingJavaScript { false };
+ [webView callAsyncJavaScript:@"return (await navigator.permissions.query({ name: \"geolocation\" })).state" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
+ EXPECT_WK_STREQ(@"prompt", result);
+ doneEvaluatingJavaScript = true;
+ }];
+ TestWebKitAPI::Util::run(&doneEvaluatingJavaScript);
+
+ done = false;
+ didReceiveMessage = false;
+ [webView evaluateJavaScript:getCurrentPosition completionHandler:nil];
+ TestWebKitAPI::Util::run(&didReceiveMessage);
+ TestWebKitAPI::Util::run(&done);
+
+ // Reset web process state.
+ [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/"]]];
+ [navigationDelegate waitForDidFinishNavigation];
+ [webView loadRequest:[NSURLRequest requestWithURL:testURL.get()]];
+ [navigationDelegate waitForDidFinishNavigation];
+
+ done = false;
+ didReceiveMessage = false;
+ doneEvaluatingJavaScript = false;
+ [webView callAsyncJavaScript:@"return (await new Promise(function (resolve, reject) {"
+ "navigator.geolocation.getCurrentPosition((position) => { resolve(JSON.stringify(position.toJSON())) }, (error) => { reject(error.message) });"
+ "}));" arguments:nil inFrame:nil inContentWorld:WKContentWorld.pageWorld completionHandler:^(id result, NSError *error) {
+ EXPECT_NULL(error);
+ EXPECT_TRUE([result isKindOfClass:[NSString class]]);
... (truncated)