Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,35 @@ public async Task ShouldFailFetchRequestsToUrlsInBlocklist()
Assert.That(fetchError, Does.Contain("Failed to fetch"));
}

[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should fail service worker registration for blocklisted script URLs")]
public async Task ShouldFailServiceWorkerRegistrationForBlocklistedScriptUrls()
{
var options = TestConstants.DefaultBrowserOptions();
options.BlockList = ["*://*:*/empty.html", "*://*:*/pptr.png", "*://*:*/serviceworkers/empty/sw.js"];

await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
await using var page = await browser.NewPageAsync();

var allowedUrl = TestConstants.ServerUrl + "/title.html";
var blockedUrl = TestConstants.ServerUrl + "/serviceworkers/empty/sw.js";

await page.GoToAsync(allowedUrl);

var swError = await page.EvaluateFunctionAsync<string>(
@"async (url) => {
try {
await navigator.serviceWorker.register(url);
return null;
} catch (e) {
return e.message;
}
}",
blockedUrl);

Assert.That(swError, Is.Not.Null.And.Not.Empty);
Assert.That(swError, Does.Contain("Failed to register a ServiceWorker"));
}

[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions blocklist validation", "should fail fetch requests from within a service worker to URLs in the blocklist")]
public async Task ShouldFailFetchRequestsFromWithinServiceWorkerToUrlsInBlocklist()
{
Expand Down Expand Up @@ -303,6 +332,35 @@ public async Task ShouldFailFetchRequestsToUrlsNotInAllowlist()
Assert.That(fetchError, Does.Contain("Failed to fetch"));
}

[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should fail service worker registration for script URLs not in the allowlist")]
public async Task ShouldFailServiceWorkerRegistrationForScriptUrlsNotInAllowlist()
{
var options = TestConstants.DefaultBrowserOptions();
options.Allowlist = ["*://*:*/empty.html"];

await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
await using var page = await browser.NewPageAsync();

var allowedUrl = TestConstants.ServerUrl + "/empty.html";
var blockedUrl = TestConstants.ServerUrl + "/serviceworkers/empty/sw.js";

await page.GoToAsync(allowedUrl);

var swError = await page.EvaluateFunctionAsync<string>(
@"async (url) => {
try {
await navigator.serviceWorker.register(url);
return null;
} catch (e) {
return e.message;
}
}",
blockedUrl);

Assert.That(swError, Is.Not.Null.And.Not.Empty);
Assert.That(swError, Does.Contain("Failed to register a ServiceWorker"));
}

[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should prevent loading of subresources not in the allowlist (e.g., images)")]
public async Task ShouldPreventLoadingOfSubresourcesNotInAllowlist()
{
Expand Down
56 changes: 53 additions & 3 deletions lib/PuppeteerSharp/Cdp/ChromeTargetManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,7 @@ private async Task OnAttachedToTargetAsync(object sender, TargetAttachedToTarget
{
if (!IsUrlAllowed(targetInfo.Url))
{
await Task.WhenAll(
MaybeSetupNetworkBlockListAsync(session, targetInfo),
session.SendAsync("Runtime.runIfWaitingForDebugger")).ConfigureAwait(false);
await BlockServiceWorkerRegistrationAsync(session).ConfigureAwait(false);
return;
}

Expand Down Expand Up @@ -512,6 +510,58 @@ private void OnDetachedFromTarget(object sender, TargetDetachedFromTargetRespons
TargetGone?.Invoke(this, new TargetChangedArgs { Target = target });
}

// Blocks the registration of a disallowed service worker by failing every request it makes
// (including its own main script fetch) at the Fetch layer. Using Fetch interception rather
// than Network.emulateNetworkConditions is deliberate: a paused worker (waitForDebuggerOnStart)
// answers no CDP command until it is resumed, so we cannot await any setup before resuming;
// and offline emulation only sets a connectivity state that races the script fetch (and on some
// transports stalls it instead of failing it). Fetch.enable is queued ahead of
// runIfWaitingForDebugger on the same session (FIFO), so interception is active before the
// worker can fetch anything, and Fetch holds each request until we fail it — no race, no stall,
// deterministic across transports. This diverges from upstream (which uses offline emulation and
// only exercises headless+websocket in CI, so it never hits the race/stall).
private async Task BlockServiceWorkerRegistrationAsync(CDPSession session)
{
async void OnRequestPaused(object sender, MessageEventArgs e)
{
if (e.MessageID != "Fetch.requestPaused")
{
return;
}

var paused = e.MessageData.ToObject<FetchRequestPausedResponse>();
try
{
await session.SendAsync(
"Fetch.failRequest",
new FetchFailRequest
{
RequestId = paused.RequestId,
ErrorReason = "BlockedByClient",
}).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to fail blocked service worker request");
}
}

session.MessageReceived += OnRequestPaused;

try
{
await Task.WhenAll(
session.SendAsync(
"Fetch.enable",
new FetchEnableRequest { Patterns = [new FetchEnableRequest.Pattern("*")] }),
session.SendAsync("Runtime.runIfWaitingForDebugger")).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to block service worker registration");
}
}

private async Task MaybeSetupNetworkBlockListAsync(CDPSession session, TargetInfo targetInfo)
{
var hasBlockList = _blockList != null && _blockList.Length > 0;
Expand Down
Loading