Skip to content

Commit

Permalink
Return empty port with a delay if destination extension isn't found f…
Browse files Browse the repository at this point in the history
…or externally_connectable.

https://webkit.org/b/269539
rdar://123060441

Reviewed by Brian Weinstein.

Enhance privacy in web-to-extension messaging by ensuring indistinguishability between scenarios
where an extension is not found or lacks permission to the page and when messaging is permitted.
This approach mitigates fingerprinting based on installed extensions.

Accomplish this by introducing a random delay for runtime.sendMessage() responses in error cases.
Also runtime.connect() now consistently returns a port, which is subsequently disconnected after
a random delay. Importantly, no errors are reported to the web page in any of these situations.

Also improved port bookkeeping by always sending the PortRemoved message (was PortDisconnect)
when the port is disconnected or garbage collected.

* Source/WebKit/Platform/cocoa/CocoaHelpers.h:
* Source/WebKit/Platform/cocoa/CocoaHelpers.mm:
(WebKit::callAfterRandomDelay): Added.
* Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIPortCocoa.mm:
(WebKit::WebExtensionContext::portRemoved): Added.
(WebKit::WebExtensionContext::portDisconnect): Deleted.
* Source/WebKit/UIProcess/Extensions/Cocoa/API/WebExtensionContextAPIRuntimeCocoa.mm:
(WebKit::WebExtensionContext::runtimeWebPageSendMessage): Added work behind callAfterRandomDelay().
(WebKit::WebExtensionContext::runtimeWebPageConnect): Ditto.
* Source/WebKit/UIProcess/Extensions/Cocoa/WebExtensionMessagePortCocoa.mm:
(WebKit::WebExtensionMessagePort::disconnect): Move portRemoved() call to remove().
(WebKit::WebExtensionMessagePort::remove): Add call to portRemoved().
* Source/WebKit/UIProcess/Extensions/WebExtensionContext.h:
* Source/WebKit/UIProcess/Extensions/WebExtensionContext.messages.in:
* Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIEventCocoa.mm:
(WebKit::WebExtensionAPIEvent::addListener): Check hasExtensionContext() before using extensionContext().
This was needed since the quarantined port has no extensionContext, and events it created don't as well.
(WebKit::WebExtensionAPIEvent::removeListener): Ditto.
(WebKit::WebExtensionAPIEvent::removeAllListeners): Ditto.
* Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIPortCocoa.mm:
(WebKit::WebExtensionAPIPort::add): ASSERT !isQuarantined(), since it should not be added to the map.
(WebKit::WebExtensionAPIPort::remove): Return early for isQuarantined(). Send PortRemoved here.
(WebKit::WebExtensionAPIPort::postMessage): Use renamed isDisconnected().
(WebKit::WebExtensionAPIPort::fireMessageEventIfNeeded): Return early for isQuarantined().
(WebKit::WebExtensionAPIPort::fireDisconnectEventIfNeeded): Moved PortDisconnect message to remove().
* Source/WebKit/WebProcess/Extensions/API/Cocoa/WebExtensionAPIRuntimeCocoa.mm:
(WebKit::WebExtensionAPIWebPageRuntime::sendMessage): Respond after a random delay.
(WebKit::WebExtensionAPIWebPageRuntime::connect): Return a port, and disconnect after a random delay.
* Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIObject.h:
(WebKit::WebExtensionAPIObject::hasExtensionContext const): Added.
* Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIPort.h:
(WebKit::WebExtensionAPIPort::isDisconnected const): Added.
(WebKit::WebExtensionAPIPort::isQuarantined const): Added.
(WebKit::WebExtensionAPIPort::WebExtensionAPIPort): Added.
(WebKit::WebExtensionAPIPort::disconnected const): Deleted.
* Tools/TestWebKitAPI/Tests/WebKitCocoa/WKWebExtensionAPIRuntime.mm:
(TEST(WKWebExtensionAPIRuntime, ConnectFromWebPageWithWrongIdentifier)): Added.
(TEST(WKWebExtensionAPIRuntime, SendMessageFromWebPageWithWrongIdentifier)): Added.

Canonical link: https://commits.webkit.org/275637@main
  • Loading branch information
xeenon committed Mar 4, 2024
1 parent c8d2cfa commit 24d05ed
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 49 deletions.
2 changes: 2 additions & 0 deletions Source/WebKit/Platform/cocoa/CocoaHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ NSURL *ensureDirectoryExists(NSURL *directory);

NSString *escapeCharactersInString(NSString *, NSString *charactersToEscape);

void callAfterRandomDelay(Function<void()>&&);

NSDate *toAPI(const WallTime&);
WallTime toImpl(NSDate *);

Expand Down
8 changes: 8 additions & 0 deletions Source/WebKit/Platform/cocoa/CocoaHelpers.mm
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#import "Logging.h"
#import "WKNSData.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import <wtf/BlockPtr.h>
#import <wtf/FileSystem.h>

namespace WebKit {
Expand Down Expand Up @@ -432,6 +433,13 @@ id parseJSON(API::Data& json, JSONOptionSet options, NSError **error)
return result;
}

void callAfterRandomDelay(Function<void()>&& completionHandler)
{
// Random delay between 100 and 500 milliseconds.
auto delay = Seconds::fromMilliseconds(100) + Seconds::fromMilliseconds((static_cast<double>(arc4random()) / static_cast<double>(UINT32_MAX)) * 400);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delay.nanosecondsAs<int64_t>()), dispatch_get_main_queue(), makeBlockPtr(WTFMove(completionHandler)).get());
}

NSDate *toAPI(const WallTime& time)
{
if (time.isNaN())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@
}
}

void WebExtensionContext::portDisconnect(WebExtensionContentWorldType sourceContentWorldType, WebExtensionContentWorldType targetContentWorldType, WebExtensionPortChannelIdentifier channelIdentifier)
void WebExtensionContext::portRemoved(WebExtensionContentWorldType sourceContentWorldType, WebExtensionContentWorldType targetContentWorldType, WebExtensionPortChannelIdentifier channelIdentifier)
{
RELEASE_LOG_DEBUG(Extensions, "Port for channel %{public}llu disconnected in %{public}@ world", channelIdentifier.toUInt64(), (NSString *)toDebugString(sourceContentWorldType));
RELEASE_LOG_DEBUG(Extensions, "Port for channel %{public}llu removed in %{public}@ world", channelIdentifier.toUInt64(), (NSString *)toDebugString(sourceContentWorldType));

removePort(sourceContentWorldType, channelIdentifier);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,16 +265,12 @@
void WebExtensionContext::runtimeWebPageSendMessage(const String& extensionID, const String& messageJSON, const WebExtensionMessageSenderParameters& senderParameters, CompletionHandler<void(Expected<String, WebExtensionError>&&)>&& completionHandler)
{
RefPtr destinationExtension = extensionController()->extensionContext(extensionID);
if (!destinationExtension) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
return;
}

RefPtr tab = getTab(senderParameters.pageProxyIdentifier);
if (!tab) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
if (!destinationExtension || !tab) {
callAfterRandomDelay([completionHandler = WTFMove(completionHandler)]() mutable {
completionHandler({ });
});

return;
}

Expand All @@ -284,8 +280,10 @@
auto url = completeSenderParameters.url;
auto validMatchPatterns = destinationExtension->extension().externallyConnectableMatchPatterns();
if (!hasPermission(url, tab.get()) || !WebExtensionMatchPattern::patternsMatchURL(validMatchPatterns, url)) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
callAfterRandomDelay([completionHandler = WTFMove(completionHandler)]() mutable {
completionHandler({ });
});

return;
}

Expand Down Expand Up @@ -316,20 +314,14 @@
constexpr auto targetContentWorldType = WebExtensionContentWorldType::Main;

RefPtr destinationExtension = extensionController()->extensionContext(extensionID);
if (!destinationExtension) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier);
clearQueuedPortMessages(targetContentWorldType, channelIdentifier);
return;
}

RefPtr tab = getTab(senderParameters.pageProxyIdentifier);
if (!tab) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier);
clearQueuedPortMessages(targetContentWorldType, channelIdentifier);
if (!destinationExtension || !tab) {
callAfterRandomDelay([=, this, protectedThis = Ref { *this }, completionHandler = WTFMove(completionHandler)]() mutable {
completionHandler({ });
firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier);
clearQueuedPortMessages(targetContentWorldType, channelIdentifier);
});

return;
}

Expand All @@ -339,10 +331,12 @@
auto url = completeSenderParameters.url;
auto validMatchPatterns = destinationExtension->extension().externallyConnectableMatchPatterns();
if (!hasPermission(url, tab.get()) || !WebExtensionMatchPattern::patternsMatchURL(validMatchPatterns, url)) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
completionHandler({ });
firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier);
clearQueuedPortMessages(targetContentWorldType, channelIdentifier);
callAfterRandomDelay([=, this, protectedThis = Ref { *this }, completionHandler = WTFMove(completionHandler)]() mutable {
completionHandler({ });
firePortDisconnectEventIfNeeded(sourceContentWorldType, targetContentWorldType, channelIdentifier);
clearQueuedPortMessages(targetContentWorldType, channelIdentifier);
});

return;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,6 @@

void WebExtensionMessagePort::disconnect(Error error)
{
if (isDisconnected())
return;

m_extensionContext->portDisconnect(WebExtensionContentWorldType::Native, WebExtensionContentWorldType::Main, m_channelIdentifier);

remove();
}

Expand All @@ -116,6 +111,7 @@
if (isDisconnected())
return;

m_extensionContext->portRemoved(WebExtensionContentWorldType::Native, WebExtensionContentWorldType::Main, m_channelIdentifier);
m_extensionContext->removeNativePort(*this);
m_extensionContext = nullptr;
}
Expand Down
2 changes: 1 addition & 1 deletion Source/WebKit/UIProcess/Extensions/WebExtensionContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ class WebExtensionContext : public API::ObjectImpl<API::Object::Type::WebExtensi

// Port APIs
void portPostMessage(WebExtensionContentWorldType targetContentWorldType, std::optional<WebKit::WebPageProxyIdentifier>, WebExtensionPortChannelIdentifier, const String& messageJSON);
void portDisconnect(WebExtensionContentWorldType sourceContentWorldType, WebExtensionContentWorldType targetContentWorldType, WebExtensionPortChannelIdentifier);
void portRemoved(WebExtensionContentWorldType sourceContentWorldType, WebExtensionContentWorldType targetContentWorldType, WebExtensionPortChannelIdentifier);
void addPorts(WebExtensionContentWorldType, WebExtensionPortChannelIdentifier, size_t totalPortObjects);
void removePort(WebExtensionContentWorldType, WebExtensionPortChannelIdentifier);
void addNativePort(WebExtensionMessagePort&);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ messages -> WebExtensionContext {

// Port APIs
PortPostMessage(WebKit::WebExtensionContentWorldType targetContentWorldType, std::optional<WebKit::WebPageProxyIdentifier> sendingPageProxyIdentifier, WebKit::WebExtensionPortChannelIdentifier channelIdentifier, String messageJSON)
PortDisconnect(WebKit::WebExtensionContentWorldType sourceContentWorldType, WebKit::WebExtensionContentWorldType targetContentWorldType, WebKit::WebExtensionPortChannelIdentifier channelIdentifier)
PortRemoved(WebKit::WebExtensionContentWorldType sourceContentWorldType, WebKit::WebExtensionContentWorldType targetContentWorldType, WebKit::WebExtensionPortChannelIdentifier channelIdentifier)

// Runtime APIs
RuntimeGetBackgroundPage() -> (Expected<std::optional<WebCore::PageIdentifier>, WebKit::WebExtensionError> result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
m_pageProxyIdentifier = page.webPageProxyIdentifier();
m_listeners.append(listener);

if (!hasExtensionContext())
return;

WebProcess::singleton().send(Messages::WebExtensionContext::AddListener(page.webPageProxyIdentifier(), m_type, contentWorldType()), extensionContext().identifier());
}

Expand All @@ -98,6 +101,9 @@

ASSERT(page.webPageProxyIdentifier() == m_pageProxyIdentifier);

if (!hasExtensionContext())
return;

WebProcess::singleton().send(Messages::WebExtensionContext::RemoveListener(m_pageProxyIdentifier, m_type, contentWorldType(), removedCount), extensionContext().identifier());
}

Expand All @@ -113,9 +119,13 @@
if (m_listeners.isEmpty())
return;

WebProcess::singleton().send(Messages::WebExtensionContext::RemoveListener(m_pageProxyIdentifier, m_type, contentWorldType(), m_listeners.size()), extensionContext().identifier());

auto removedCount = m_listeners.size();
m_listeners.clear();

if (!hasExtensionContext())
return;

WebProcess::singleton().send(Messages::WebExtensionContext::RemoveListener(m_pageProxyIdentifier, m_type, contentWorldType(), removedCount), extensionContext().identifier());
}

} // namespace WebKit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@

void WebExtensionAPIPort::add()
{
ASSERT(!isQuarantined());

auto addResult = webExtensionPorts().ensure(channelIdentifier(), [&] {
return HashSet<WeakRef<WebExtensionAPIPort>> { };
});
Expand All @@ -82,10 +84,15 @@
{
disconnect();

if (isQuarantined())
return;

auto entry = webExtensionPorts().find(channelIdentifier());
if (entry == webExtensionPorts().end())
return;

WebProcess::singleton().send(Messages::WebExtensionContext::PortRemoved(contentWorldType(), targetContentWorldType(), channelIdentifier()), extensionContext().identifier());

entry->value.remove(*this);

if (!entry->value.isEmpty())
Expand Down Expand Up @@ -118,7 +125,7 @@
{
// Documentation: https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port#postmessage

if (disconnected()) {
if (isDisconnected()) {
*outExceptionString = toErrorString(nil, nil, @"the port is disconnected");
return;
}
Expand All @@ -128,6 +135,9 @@
return;
}

if (isQuarantined())
return;

RELEASE_LOG_DEBUG(Extensions, "Sent port message for channel %{public}llu from %{public}@ world", channelIdentifier().toUInt64(), (NSString *)toDebugString(contentWorldType()));

WebProcess::singleton().send(Messages::WebExtensionContext::PortPostMessage(targetContentWorldType(), owningPageProxyIdentifier(), channelIdentifier(), message), extensionContext().identifier());
Expand All @@ -142,7 +152,7 @@

void WebExtensionAPIPort::fireMessageEventIfNeeded(id message)
{
if (disconnected() || !m_onMessage || m_onMessage->listeners().isEmpty())
if (isDisconnected() || isQuarantined() || !m_onMessage || m_onMessage->listeners().isEmpty())
return;

RELEASE_LOG_DEBUG(Extensions, "Fired port message event for channel %{public}llu in %{public}@ world", channelIdentifier().toUInt64(), (NSString *)toDebugString(contentWorldType()));
Expand All @@ -157,7 +167,7 @@

void WebExtensionAPIPort::fireDisconnectEventIfNeeded()
{
if (disconnected())
if (isDisconnected())
return;

RELEASE_LOG_DEBUG(Extensions, "Port channel %{public}llu disconnected in %{public}@ world", channelIdentifier().toUInt64(), (NSString *)toDebugString(contentWorldType()));
Expand All @@ -169,8 +179,6 @@

remove();

WebProcess::singleton().send(Messages::WebExtensionContext::PortDisconnect(contentWorldType(), targetContentWorldType(), channelIdentifier()), extensionContext().identifier());

if (!m_onDisconnect || m_onDisconnect->listeners().isEmpty())
return;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,11 @@ - (instancetype)initWithAggregator:(WebKit::ReplyCallbackAggregator&)aggregator
Ref page = *frame.page();
RefPtr destinationExtensionContext = page->webExtensionControllerProxy()->extensionContext(extensionID);
if (!destinationExtensionContext) {
// FIXME: <https://webkit.org/b/269539> Return after a random delay.
callback->call();
// Respond after a random delay to prevent the page from easily detecting if extensions are not installed.
callAfterRandomDelay([callback = WTFMove(callback)]() {
callback->call();
});

return;
}

Expand Down Expand Up @@ -448,8 +451,14 @@ - (instancetype)initWithAggregator:(WebKit::ReplyCallbackAggregator&)aggregator
Ref page = *frame.page();
RefPtr destinationExtensionContext = page->webExtensionControllerProxy()->extensionContext(extensionID);
if (!destinationExtensionContext) {
// FIXME: <https://webkit.org/b/269539> Return a port that disconnects after a random delay.
return nullptr;
// Return a port that cant send messages, and disconnect after a random delay to prevent the page from easily detecting if extensions are not installed.
Ref port = WebExtensionAPIPort::create(*this, resolvedName);

callAfterRandomDelay([=]() {
port->disconnect();
});

return port;
}

Ref port = WebExtensionAPIPort::create(contentWorldType(), runtime(), *destinationExtensionContext, page, WebExtensionContentWorldType::Main, resolvedName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ class WebExtensionAPIObject {
virtual WebExtensionAPIRuntimeBase& runtime() const { return *m_runtime; }
WebExtensionContextProxy& extensionContext() const { return *m_extensionContext; }

bool hasExtensionContext() const { return !!m_extensionContext; }

private:
WebExtensionContentWorldType m_contentWorldType { WebExtensionContentWorldType::Main };
RefPtr<WebExtensionAPIRuntimeBase> m_runtime;
Expand Down
11 changes: 10 additions & 1 deletion Source/WebKit/WebProcess/Extensions/API/WebExtensionAPIPort.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class WebExtensionAPIPort : public WebExtensionAPIObject, public JSWebExtensionW
void postMessage(WebFrame&, NSString *, NSString **outExceptionString);
void disconnect();

bool disconnected() const { return m_disconnected; }
bool isDisconnected() const { return m_disconnected; }
bool isQuarantined() const { return !m_channelIdentifier; }

NSString *name();
NSDictionary *sender();
Expand All @@ -80,6 +81,14 @@ class WebExtensionAPIPort : public WebExtensionAPIObject, public JSWebExtensionW
private:
friend class WebExtensionContextProxy;

explicit WebExtensionAPIPort(const WebExtensionAPIObject& parentObject, const String& name)
: WebExtensionAPIObject(parentObject)
, m_targetContentWorldType(WebExtensionContentWorldType::Main)
, m_name(name)
{
ASSERT(isQuarantined());
}

explicit WebExtensionAPIPort(const WebExtensionAPIObject& parentObject, WebPage& page, WebExtensionContentWorldType targetContentWorldType, const String& name)
: WebExtensionAPIObject(parentObject)
, m_targetContentWorldType(targetContentWorldType)
Expand Down
Loading

0 comments on commit 24d05ed

Please sign in to comment.