Skip to content

Commit 406c340

Browse files
kblokclaude
andauthored
feat: implement URL blocklist to restrict access to unauthorized sites (#14873) (#3429)
Ports upstream Puppeteer PR #14873 which adds a `BlockList` option to `ConnectOptions` and `LaunchOptions`. When set, the browser will silently detach from any already-open targets violating the patterns on initial attachment, and fail network requests matching blocked URL patterns using `Network.emulateNetworkConditionsByRule` (CDP only). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7262801 commit 406c340

10 files changed

Lines changed: 400 additions & 7 deletions

File tree

lib/PuppeteerSharp.Nunit/TestExpectations/TestExpectations.upstream.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,66 @@
310310
],
311311
"comment": "TODO: add a comment explaining why this expectation is required (include links to issues)"
312312
},
313+
{
314+
"testIdPattern": "[network_restrictions.spec] *",
315+
"platforms": [
316+
"darwin",
317+
"linux",
318+
"win32"
319+
],
320+
"parameters": [
321+
"firefox"
322+
],
323+
"expectations": [
324+
"SKIP"
325+
],
326+
"comment": "CDP-specific feature, not supported in Firefox"
327+
},
328+
{
329+
"testIdPattern": "[network_restrictions.spec] *",
330+
"platforms": [
331+
"darwin",
332+
"linux",
333+
"win32"
334+
],
335+
"parameters": [
336+
"webDriverBiDi"
337+
],
338+
"expectations": [
339+
"SKIP"
340+
],
341+
"comment": "CDP-specific feature, not supported in WebDriver BiDi"
342+
},
343+
{
344+
"testIdPattern": "[network_restrictions.spec] *",
345+
"platforms": [
346+
"darwin",
347+
"linux",
348+
"win32"
349+
],
350+
"parameters": [
351+
"chrome-headless-shell"
352+
],
353+
"expectations": [
354+
"SKIP"
355+
],
356+
"comment": "Not supported by chrome-headless-shell"
357+
},
358+
{
359+
"testIdPattern": "[network_restrictions.spec] Network Restrictions should detach from targets violating blocklist when connecting to a running browser",
360+
"platforms": [
361+
"darwin",
362+
"linux",
363+
"win32"
364+
],
365+
"parameters": [
366+
"pipe"
367+
],
368+
"expectations": [
369+
"SKIP"
370+
],
371+
"comment": "Verifies URL blocklist enforcement when connecting to a browser via WebSocket, which is not applicable for pipe mode"
372+
},
313373
{
314374
"testIdPattern": "[network.spec] network Page.setBypassServiceWorker *",
315375
"platforms": [
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using NUnit.Framework;
5+
using PuppeteerSharp.Nunit;
6+
7+
namespace PuppeteerSharp.Tests.NetworkRestrictionTests;
8+
9+
public class NetworkRestrictionsTests : PuppeteerBaseTest
10+
{
11+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should block page.goto when the destination is in the blocklist")]
12+
public async Task ShouldBlockPageGotoWhenDestinationIsInBlocklist()
13+
{
14+
var options = TestConstants.DefaultBrowserOptions();
15+
options.BlockList = ["*://*:*/empty.html"];
16+
17+
await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
18+
await using var page = await browser.NewPageAsync();
19+
20+
var allowedUrl = TestConstants.ServerUrl + "/title.html";
21+
var blockedUrl = TestConstants.ServerUrl + "/empty.html";
22+
23+
await page.GoToAsync(allowedUrl);
24+
25+
Exception error = null;
26+
await page.GoToAsync(blockedUrl).ContinueWith(t =>
27+
{
28+
if (t.IsFaulted)
29+
{
30+
error = t.Exception?.InnerException ?? t.Exception;
31+
}
32+
33+
return t;
34+
});
35+
36+
Assert.That(error, Is.Not.Null);
37+
Assert.That(error.Message, Does.Contain("net::ERR_INTERNET_DISCONNECTED"));
38+
}
39+
40+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should block window.location.href navigation to URLs in the blocklist")]
41+
public async Task ShouldBlockWindowLocationHrefNavigationToUrlsInBlocklist()
42+
{
43+
var options = TestConstants.DefaultBrowserOptions();
44+
options.BlockList = ["*://*:*/empty.html"];
45+
46+
await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
47+
await using var page = await browser.NewPageAsync();
48+
49+
var allowedUrl = TestConstants.ServerUrl + "/title.html";
50+
var blockedUrl = TestConstants.ServerUrl + "/empty.html";
51+
52+
await page.GoToAsync(allowedUrl);
53+
54+
var navTask = page.WaitForNavigationAsync(new NavigationOptions { Timeout = 2000 }).ContinueWith(t => t.IsFaulted ? null : t.Result);
55+
await page.EvaluateFunctionAsync("url => { window.location.href = url; }", blockedUrl);
56+
await navTask;
57+
58+
var finalUrl = page.Url;
59+
Assert.That(finalUrl, Is.Not.EqualTo(blockedUrl));
60+
}
61+
62+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should fail fetch requests to URLs in the blocklist")]
63+
public async Task ShouldFailFetchRequestsToUrlsInBlocklist()
64+
{
65+
var options = TestConstants.DefaultBrowserOptions();
66+
options.BlockList = ["*://*:*/empty.html"];
67+
68+
await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
69+
await using var page = await browser.NewPageAsync();
70+
71+
var allowedUrl = TestConstants.ServerUrl + "/title.html";
72+
var blockedUrl = TestConstants.ServerUrl + "/empty.html";
73+
74+
await page.GoToAsync(allowedUrl);
75+
76+
var fetchError = await page.EvaluateFunctionAsync<string>(
77+
@"async (url) => {
78+
try {
79+
await fetch(url);
80+
return null;
81+
} catch (e) {
82+
return e.message;
83+
}
84+
}",
85+
blockedUrl);
86+
87+
Assert.That(fetchError, Is.Not.Null.And.Not.Empty);
88+
Assert.That(fetchError, Does.Contain("Failed to fetch"));
89+
}
90+
91+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should prevent loading of blocklisted subresources (e.g., images)")]
92+
public async Task ShouldPreventLoadingOfBlocklistedSubresources()
93+
{
94+
var options = TestConstants.DefaultBrowserOptions();
95+
options.BlockList = ["*://*:*/pptr.png"];
96+
97+
await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
98+
await using var page = await browser.NewPageAsync();
99+
100+
var allowedUrl = TestConstants.ServerUrl + "/one-style.css";
101+
var blockedUrl = TestConstants.ServerUrl + "/pptr.png";
102+
103+
var failedRequests = new Dictionary<string, string>();
104+
var finishedRequests = new HashSet<string>();
105+
106+
page.RequestFailed += (_, e) =>
107+
{
108+
failedRequests[e.Request.Url] = e.Request.FailureText;
109+
};
110+
page.RequestFinished += (_, e) =>
111+
{
112+
finishedRequests.Add(e.Request.Url);
113+
};
114+
115+
await page.GoToAsync(TestConstants.EmptyPage);
116+
117+
await page.SetContentAsync(
118+
$@"<img src=""{blockedUrl}"" />
119+
<link rel=""stylesheet"" href=""{allowedUrl}"" />",
120+
new NavigationOptions { WaitUntil = [WaitUntilNavigation.Networkidle0] });
121+
122+
Assert.That(failedRequests.ContainsKey(blockedUrl), Is.True);
123+
Assert.That(failedRequests[blockedUrl], Does.Contain("net::ERR_INTERNET_DISCONNECTED"));
124+
Assert.That(finishedRequests.Contains(allowedUrl), Is.True);
125+
}
126+
127+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should detach from targets violating blocklist when connecting to a running browser")]
128+
public async Task ShouldDetachFromTargetsViolatingBlocklistWhenConnectingToRunningBrowser()
129+
{
130+
await using var originalBrowser = await Puppeteer.LaunchAsync(TestConstants.DefaultBrowserOptions(), TestConstants.LoggerFactory);
131+
var blockedUrl = TestConstants.ServerUrl + "/empty.html";
132+
133+
var page = await originalBrowser.NewPageAsync();
134+
await page.GoToAsync(blockedUrl);
135+
136+
var wsEndpoint = originalBrowser.WebSocketEndpoint;
137+
138+
IBrowser connectedBrowser = null;
139+
try
140+
{
141+
connectedBrowser = await Puppeteer.ConnectAsync(new ConnectOptions
142+
{
143+
BrowserWSEndpoint = wsEndpoint,
144+
BlockList = ["*://*:*/empty.html"],
145+
});
146+
147+
var targets = connectedBrowser.Targets();
148+
var blockedTarget = Array.Find(targets, t => t.Url == blockedUrl);
149+
150+
Assert.That(blockedTarget, Is.Null);
151+
}
152+
finally
153+
{
154+
connectedBrowser?.Disconnect();
155+
await page.CloseAsync();
156+
}
157+
}
158+
159+
[Test, PuppeteerTest("network_restrictions.spec", "Network Restrictions", "should not block chrome://version/ even if it matches blocklist")]
160+
public async Task ShouldNotBlockChromeVersionEvenIfItMatchesBlocklist()
161+
{
162+
const string chromeUrl = "chrome://version/";
163+
var options = TestConstants.DefaultBrowserOptions();
164+
options.BlockList = [chromeUrl];
165+
166+
await using var browser = await Puppeteer.LaunchAsync(options, TestConstants.LoggerFactory);
167+
await using var page = await browser.NewPageAsync();
168+
169+
await page.GoToAsync(chromeUrl);
170+
171+
// Navigation should succeed as chrome:// URLs usually bypass the network
172+
Assert.That(page.Url, Is.EqualTo(chromeUrl));
173+
}
174+
}

lib/PuppeteerSharp/Cdp/CdpBrowser.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ internal CdpBrowser(
5252
Func<Target, bool> isPageTargetFunc = null,
5353
bool handleDevToolsAsPage = false,
5454
bool networkEnabled = true,
55-
bool issuesEnabled = true)
55+
bool issuesEnabled = true,
56+
string[] blockList = null)
5657
{
5758
BrowserType = browser;
5859
DefaultViewport = defaultViewport;
@@ -85,7 +86,8 @@ internal CdpBrowser(
8586
TargetManager = new ChromeTargetManager(
8687
connection,
8788
CreateTarget,
88-
targetFilterCallback);
89+
targetFilterCallback,
90+
blockList);
8991
}
9092
}
9193

@@ -264,7 +266,8 @@ internal static async Task<CdpBrowser> CreateAsync(
264266
Action<IBrowser> initAction = null,
265267
bool handleDevToolsAsPage = false,
266268
bool networkEnabled = true,
267-
bool issuesEnabled = true)
269+
bool issuesEnabled = true,
270+
string[] blockList = null)
268271
{
269272
var browser = new CdpBrowser(
270273
browserToCreate,
@@ -277,7 +280,8 @@ internal static async Task<CdpBrowser> CreateAsync(
277280
isPageTargetCallback,
278281
handleDevToolsAsPage,
279282
networkEnabled,
280-
issuesEnabled);
283+
issuesEnabled,
284+
blockList);
281285

282286
try
283287
{

0 commit comments

Comments
 (0)