Skip to content

Commit 2707cbf

Browse files
committed
Core - FolderSchemeHandlerFactory improve path check to prevent break out
- Prevent accessing resources outside of the root path specified. - Add Test to verify
1 parent e4fb858 commit 2707cbf

2 files changed

Lines changed: 78 additions & 9 deletions

File tree

CefSharp.Test/SchemeHandler/FolderSchemeHandlerFactoryTests.cs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using Xunit.Abstractions;
2-
using Xunit;
1+
using System;
2+
using System.IO;
33
using System.Threading.Tasks;
4-
using CefSharp.OffScreen;
54
using CefSharp.Example;
5+
using CefSharp.OffScreen;
66
using CefSharp.SchemeHandler;
7-
using System.IO;
7+
using Xunit;
8+
using Xunit.Abstractions;
89

910
namespace CefSharp.Test.SchemeHandler
1011
{
@@ -56,6 +57,64 @@ public async Task ShouldWork()
5657
}
5758
}
5859

60+
[Fact]
61+
public async Task ShouldPreventPathTraversalAttack()
62+
{
63+
const string hostUrl = "https://folderschemehandlerfactory.test/";
64+
65+
// 1. Setup temporary directory structure
66+
var tempParent = Path.Combine(Path.GetTempPath(), "CefSharpShouldPreventPathTraversalAttack-" + Guid.NewGuid());
67+
var root = Path.Combine(tempParent, "www");
68+
var sibling = Path.Combine(tempParent, "www2");
69+
70+
try
71+
{
72+
Directory.CreateDirectory(root);
73+
Directory.CreateDirectory(sibling);
74+
75+
File.WriteAllText(Path.Combine(root, "index.html"), "root-index");
76+
File.WriteAllText(Path.Combine(sibling, "secret.txt"), "sibling-secret");
77+
78+
// 2. Initialize the CefSharp context and browser instances
79+
using (var requestContext = new RequestContext(Cef.GetGlobalRequestContext()))
80+
using (var browser = new ChromiumWebBrowser(CefExample.DefaultUrl, requestContext: requestContext, useLegacyRenderHandler: false))
81+
{
82+
_ = await browser.WaitForInitialLoadAsync();
83+
84+
// Register factory targeting our custom root directory
85+
requestContext.RegisterSchemeHandlerFactory(
86+
"https",
87+
"folderschemehandlerfactory.test",
88+
new FolderSchemeHandlerFactory(root, defaultPage: "index.html"));
89+
90+
// 3. Attempt to break out of 'www' using an escaped path traversal sequence
91+
var traversalUrl = hostUrl + "..%2fwww2/secret.txt";
92+
var response = await browser.LoadUrlAsync(traversalUrl);
93+
94+
var mainFrame = browser.GetMainFrame();
95+
Assert.True(mainFrame.IsValid);
96+
97+
// 4. Security Assertions: The factory should sanitize the path and return a 404.
98+
// If the code is secure, HttpStatusCode should be 404 (NotFound).
99+
Assert.Equal(404, response.HttpStatusCode);
100+
101+
// Fetch DOM contents to double-check that the file contents leaked nowhere
102+
var jsResponse = await browser.EvaluateScriptAsync("document.documentElement.innerText");
103+
var bodyText = jsResponse.Result?.ToString() ?? string.Empty;
104+
105+
Assert.DoesNotContain("sibling-secret", bodyText);
106+
}
107+
}
108+
finally
109+
{
110+
// 5. Clean up temporary directories and files safely
111+
if (Directory.Exists(tempParent))
112+
{
113+
Directory.Delete(tempParent, recursive: true);
114+
}
115+
}
116+
}
117+
59118
[Fact]
60119
public async Task ShouldAllowFileDeletionAfterLoading()
61120
{

CefSharp/SchemeHandler/FolderSchemeHandlerFactory.cs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public FolderSchemeHandlerFactory(string rootFolder, string schemeName = null, s
5050
{
5151
throw new DirectoryNotFoundException(this.rootFolder);
5252
}
53+
54+
this.rootFolder = this.rootFolder.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
5355
}
5456

5557
/// <summary>
@@ -102,14 +104,22 @@ protected virtual IResourceHandler Create(IBrowser browser, IFrame frame, string
102104
}
103105

104106
//Get the absolute path and remove the leading slash
105-
var asbolutePath = uri.AbsolutePath.Substring(1);
107+
var absolutePath = uri.AbsolutePath.Substring(1);
108+
109+
if (string.IsNullOrEmpty(absolutePath))
110+
{
111+
absolutePath = defaultPage;
112+
}
113+
114+
var decodedPath = WebUtility.UrlDecode(absolutePath);
106115

107-
if (string.IsNullOrEmpty(asbolutePath))
116+
// Block null bytes and NTFS Alternate Data Streams (:)
117+
if (decodedPath.Contains("\0") || decodedPath.Contains(":"))
108118
{
109-
asbolutePath = defaultPage;
119+
return ResourceHandler.ForErrorMessage($"File Not Found - {absolutePath}", HttpStatusCode.NotFound);
110120
}
111121

112-
var filePath = Path.GetFullPath(Path.Combine(rootFolder, WebUtility.UrlDecode(asbolutePath)));
122+
var filePath = Path.GetFullPath(Path.Combine(rootFolder, decodedPath));
113123

114124
//Check the file requested is within the specified path and that the file exists
115125
if (filePath.StartsWith(rootFolder, StringComparison.OrdinalIgnoreCase) && File.Exists(filePath))
@@ -120,7 +130,7 @@ protected virtual IResourceHandler Create(IBrowser browser, IFrame frame, string
120130
return ResourceHandler.FromStream(stream, mimeType, autoDisposeStream: true);
121131
}
122132

123-
return ResourceHandler.ForErrorMessage("File Not Found - " + filePath, HttpStatusCode.NotFound);
133+
return ResourceHandler.ForErrorMessage($"File Not Found - {absolutePath}", HttpStatusCode.NotFound);
124134
}
125135
}
126136
}

0 commit comments

Comments
 (0)