Skip to content

Commit 3e985c5

Browse files
Meir017kblok
authored andcommitted
CSP support & Page.setBypassCSP (#418)
1 parent 64a2b19 commit 3e985c5

File tree

10 files changed

+204
-25
lines changed

10 files changed

+204
-25
lines changed

lib/PuppeteerSharp.TestServer/PuppeteerSharp.TestServer.csproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@
44
<OutputType>Library</OutputType>
55
</PropertyGroup>
66

7-
<ItemGroup>
8-
<Folder Include="wwwroot\" />
9-
</ItemGroup>
10-
117
<ItemGroup>
128
<None Remove="testCert.cer" />
139
</ItemGroup>

lib/PuppeteerSharp.TestServer/SimpleServer.cs

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ public class SimpleServer
1717
private readonly IDictionary<string, Action<HttpRequest>> _requestSubscribers;
1818
private readonly IDictionary<string, RequestDelegate> _routes;
1919
private readonly IDictionary<string, (string username, string password)> _auths;
20+
private readonly IDictionary<string, string> _csp;
2021
private readonly IWebHost _webHost;
2122

2223
public static SimpleServer Create(int port, string contentRoot) => new SimpleServer(port, contentRoot, isHttps: false);
@@ -27,6 +28,7 @@ public SimpleServer(int port, string contentRoot, bool isHttps)
2728
_requestSubscribers = new ConcurrentDictionary<string, Action<HttpRequest>>();
2829
_routes = new ConcurrentDictionary<string, RequestDelegate>();
2930
_auths = new ConcurrentDictionary<string, (string username, string password)>();
31+
_csp = new ConcurrentDictionary<string, string>();
3032
_webHost = new WebHostBuilder()
3133
.ConfigureAppConfiguration((context, builder) => builder
3234
.SetBasePath(context.HostingEnvironment.ContentRootPath)
@@ -50,7 +52,16 @@ public SimpleServer(int port, string contentRoot, bool isHttps)
5052
}
5153
return next();
5254
})
53-
.UseStaticFiles())
55+
.UseStaticFiles(new StaticFileOptions
56+
{
57+
OnPrepareResponse = fileResponseContext =>
58+
{
59+
if(_csp.TryGetValue(fileResponseContext.Context.Request.Path, out var csp))
60+
{
61+
fileResponseContext.Context.Response.Headers["Content-Security-Policy"] = csp;
62+
}
63+
}
64+
}))
5465
.UseKestrel(options =>
5566
{
5667
if (isHttps)
@@ -66,10 +77,9 @@ public SimpleServer(int port, string contentRoot, bool isHttps)
6677
.Build();
6778
}
6879

69-
public void SetAuth(string path, string username, string password)
70-
{
71-
_auths.Add(path, (username, password));
72-
}
80+
public void SetAuth(string path, string username, string password) => _auths.Add(path, (username, password));
81+
82+
public void SetCSP(string path, string csp) => _csp.Add(path, csp);
7383

7484
public Task StartAsync() => _webHost.StartAsync();
7585

@@ -84,26 +94,21 @@ public void Reset()
8494
{
8595
_routes.Clear();
8696
_auths.Clear();
97+
_csp.Clear();
8798
foreach (var subscriber in _requestSubscribers.Values)
8899
{
89100
subscriber(null);
90101
}
91102
_requestSubscribers.Clear();
92103
}
93104

94-
public void SetRoute(string path, RequestDelegate handler)
95-
{
96-
_routes.Add(path, handler);
97-
}
105+
public void SetRoute(string path, RequestDelegate handler) => _routes.Add(path, handler);
98106

99-
public void SetRedirect(string from, string to)
107+
public void SetRedirect(string from, string to) => SetRoute(from, context =>
100108
{
101-
SetRoute(from, context =>
102-
{
103-
context.Response.Redirect(to);
104-
return Task.CompletedTask;
105-
});
106-
}
109+
context.Response.Redirect(to);
110+
return Task.CompletedTask;
111+
});
107112

108113
public async Task<T> WaitForRequest<T>(string path, Func<HttpRequest, T> selector)
109114
{
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">

lib/PuppeteerSharp.Tests/FrameTests/WaitForFunctionTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ public async Task ShouldPollOnRaf()
4949
await watchdog;
5050
}
5151

52+
[Fact]
53+
public async Task ShouldWorkWithStrictCSPPolicy()
54+
{
55+
Server.SetCSP("/empty.html", "script-src " + TestConstants.ServerUrl);
56+
await Page.GoToAsync(TestConstants.EmptyPage);
57+
var watchdog = Page.WaitForFunctionAsync("() => window.__FOO === 'hit'", new WaitForFunctionOptions
58+
{
59+
Polling = WaitForFunctionPollingOption.Raf
60+
});
61+
await Page.EvaluateExpressionAsync("window.__FOO = 'hit'");
62+
await watchdog;
63+
}
64+
5265
[Fact]
5366
public async Task ShouldThrowNegativePollingInterval()
5467
{

lib/PuppeteerSharp.Tests/PageTests/AddScriptTagTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,29 @@ public async Task ShouldWorkWithContent()
105105
Assert.NotNull(scriptHandle as ElementHandle);
106106
Assert.Equal(35, await Page.EvaluateExpressionAsync<int>("__injected"));
107107
}
108+
109+
[Fact]
110+
public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
111+
{
112+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
113+
var exception = await Assert.ThrowsAsync<EvaluationFailedException>(
114+
() => Page.AddScriptTagAsync(new AddTagOptions
115+
{
116+
Content = "window.__injected = 35;"
117+
}));
118+
Assert.NotNull(exception);
119+
}
120+
121+
[Fact]
122+
public async Task ShouldThrowWhenAddedWithURLToTheCSPPage()
123+
{
124+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
125+
var exception = await Assert.ThrowsAsync<PuppeteerException>(
126+
() => Page.AddScriptTagAsync(new AddTagOptions
127+
{
128+
Url = TestConstants.CrossProcessUrl + "/injectedfile.js"
129+
}));
130+
Assert.NotNull(exception);
131+
}
108132
}
109133
}

lib/PuppeteerSharp.Tests/PageTests/AddStyleTagTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,29 @@ public async Task ShouldWorkWithContent()
7272
Assert.Equal("rgb(0, 128, 0)", await Page.EvaluateExpressionAsync(
7373
"window.getComputedStyle(document.querySelector('body')).getPropertyValue('background-color')"));
7474
}
75+
76+
[Fact]
77+
public async Task ShouldThrowWhenAddedWithContentToTheCSPPage()
78+
{
79+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
80+
var exception = await Assert.ThrowsAsync<EvaluationFailedException>(
81+
() => Page.AddStyleTagAsync(new AddTagOptions
82+
{
83+
Content = "body { background-color: green; }"
84+
}));
85+
Assert.NotNull(exception);
86+
}
87+
88+
[Fact]
89+
public async Task ShouldThrowWhenAddedWithURLToTheCSPPage()
90+
{
91+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
92+
var exception = await Assert.ThrowsAsync<PuppeteerException>(
93+
() => Page.AddStyleTagAsync(new AddTagOptions
94+
{
95+
Url = TestConstants.CrossProcessUrl + "/injectedstyle.css"
96+
}));
97+
Assert.NotNull(exception);
98+
}
7599
}
76100
}

lib/PuppeteerSharp.Tests/PageTests/EvaluateOnNewDocumentTests.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,23 @@ await Page.EvaluateOnNewDocumentAsync(@"function(){
2424
await Page.GoToAsync(TestConstants.ServerUrl + "/tamperable.html");
2525
Assert.Equal(123, await Page.EvaluateExpressionAsync<int>("window.result"));
2626
}
27+
28+
[Fact]
29+
public async Task ShouldWorkWithCSP()
30+
{
31+
Server.SetCSP("/empty.html", "script-src " + TestConstants.ServerUrl);
32+
await Page.EvaluateOnNewDocumentAsync(@"function(){
33+
window.injected = 123;
34+
}");
35+
await Page.GoToAsync(TestConstants.EmptyPage);
36+
Assert.Equal(123, await Page.EvaluateExpressionAsync<int>("window.injected"));
37+
38+
// Make sure CSP works.
39+
await Page.AddScriptTagAsync(new AddTagOptions
40+
{
41+
Content = "window.e = 10;"
42+
}).ContinueWith(_ => Task.CompletedTask);
43+
Assert.Null(await Page.EvaluateExpressionAsync("window.e"));
44+
}
2745
}
2846
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
using System.Threading.Tasks;
2+
using Xunit;
3+
using Xunit.Abstractions;
4+
5+
namespace PuppeteerSharp.Tests.PageTests
6+
{
7+
[Collection("PuppeteerLoaderFixture collection")]
8+
public class SetBypassCSPTests : PuppeteerPageBaseTest
9+
{
10+
public SetBypassCSPTests(ITestOutputHelper output) : base(output)
11+
{
12+
}
13+
14+
[Fact]
15+
public async Task ShouldBypassCSPMetaTag()
16+
{
17+
// Make sure CSP prohibits addScriptTag.
18+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
19+
await Page.AddScriptTagAsync(new AddTagOptions
20+
{
21+
Content = "window.__injected = 42;"
22+
}).ContinueWith(_ => Task.CompletedTask);
23+
Assert.Null(await Page.EvaluateExpressionAsync("window.__injected"));
24+
25+
// By-pass CSP and try one more time.
26+
await Page.SetBypassCSPAsync(true);
27+
await Page.ReloadAsync();
28+
await Page.AddScriptTagAsync(new AddTagOptions
29+
{
30+
Content = "window.__injected = 42;"
31+
});
32+
Assert.Equal(42, await Page.EvaluateExpressionAsync<int>("window.__injected"));
33+
}
34+
35+
[Fact]
36+
public async Task ShouldBypassCSPHeader()
37+
{
38+
// Make sure CSP prohibits addScriptTag.
39+
Server.SetCSP("/empty.html", "default-src 'self'");
40+
await Page.GoToAsync(TestConstants.EmptyPage);
41+
await Page.AddScriptTagAsync(new AddTagOptions
42+
{
43+
Content = "window.__injected = 42;"
44+
}).ContinueWith(_ => Task.CompletedTask);
45+
Assert.Null(await Page.EvaluateExpressionAsync("window.__injected"));
46+
47+
// By-pass CSP and try one more time.
48+
await Page.SetBypassCSPAsync(true);
49+
await Page.ReloadAsync();
50+
await Page.AddScriptTagAsync(new AddTagOptions
51+
{
52+
Content = "window.__injected = 42;"
53+
});
54+
Assert.Equal(42, await Page.EvaluateExpressionAsync<int>("window.__injected"));
55+
}
56+
57+
[Fact]
58+
public async Task ShouldBypassAfterCrossProcessNavigation()
59+
{
60+
await Page.SetBypassCSPAsync(true);
61+
await Page.GoToAsync(TestConstants.ServerUrl + "/csp.html");
62+
await Page.AddScriptTagAsync(new AddTagOptions
63+
{
64+
Content = "window.__injected = 42;"
65+
});
66+
Assert.Equal(42, await Page.EvaluateExpressionAsync<int>("window.__injected"));
67+
68+
await Page.GoToAsync(TestConstants.CrossProcessUrl+ "/csp.html");
69+
await Page.AddScriptTagAsync(new AddTagOptions
70+
{
71+
Content = "window.__injected = 42;"
72+
});
73+
Assert.Equal(42, await Page.EvaluateExpressionAsync<int>("window.__injected"));
74+
}
75+
}
76+
}

lib/PuppeteerSharp/Frame.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -362,18 +362,24 @@ public async Task<ElementHandle> AddStyleTag(AddTagOptions options)
362362
const link = document.createElement('link');
363363
link.rel = 'stylesheet';
364364
link.href = url;
365-
document.head.appendChild(link);
366-
await new Promise((res, rej) => {
365+
const promise = new Promise((res, rej) => {
367366
link.onload = res;
368367
link.onerror = rej;
369368
});
369+
document.head.appendChild(link);
370+
await promise;
370371
return link;
371372
}";
372-
const string addStyleContent = @"function addStyleContent(content) {
373+
const string addStyleContent = @"async function addStyleContent(content) {
373374
const style = document.createElement('style');
374375
style.type = 'text/css';
375376
style.appendChild(document.createTextNode(content));
377+
const promise = new Promise((res, rej) => {
378+
style.onload = res;
379+
style.onerror = rej;
380+
});
376381
document.head.appendChild(style);
382+
await promise;
377383
return style;
378384
}";
379385

@@ -422,18 +428,23 @@ public async Task<ElementHandle> AddScriptTag(AddTagOptions options)
422428
script.src = url;
423429
if(type)
424430
script.type = type;
425-
document.head.appendChild(script);
426-
await new Promise((res, rej) => {
431+
const promise = new Promise((res, rej) => {
427432
script.onload = res;
428433
script.onerror = rej;
429434
});
435+
document.head.appendChild(script);
436+
await promise;
430437
return script;
431438
}";
432439
const string addScriptContent = @"function addScriptContent(content, type = 'text/javascript') {
433440
const script = document.createElement('script');
434441
script.type = type;
435442
script.text = content;
443+
let error = null;
444+
script.onerror = e => error = e;
436445
document.head.appendChild(script);
446+
if (error)
447+
throw error;
437448
return script;
438449
}";
439450

lib/PuppeteerSharp/Page.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,6 +842,17 @@ public async Task<byte[]> PdfDataAsync(PdfOptions options)
842842
public Task SetJavaScriptEnabledAsync(bool enabled)
843843
=> Client.SendAsync("Emulation.setScriptExecutionDisabled", new { value = !enabled });
844844

845+
/// <summary>
846+
/// Toggles bypassing page's Content-Security-Policy.
847+
/// </summary>
848+
/// <param name="enabled">sets bypassing of page's Content-Security-Policy.</param>
849+
/// <returns></returns>
850+
/// <remarks>
851+
/// CSP bypassing happens at the moment of CSP initialization rather then evaluation.
852+
/// Usually this means that <see cref="SetBypassCSPAsync(bool)"/> should be called before navigating to the domain.
853+
/// </remarks>
854+
public Task SetBypassCSPAsync(bool enabled) => Client.SendAsync("Page.setBypassCSP", new { enabled });
855+
845856
/// <summary>
846857
/// Emulates a media such as screen or print.
847858
/// </summary>

0 commit comments

Comments
 (0)