Skip to content

Commit 1966698

Browse files
committed
fix(security): reject absolute paths in FileResourceLoader
Prevent path traversal attacks by blocking absolute paths and verifying resolved paths stay within the configured BasePath directory.
1 parent f75a7c5 commit 1966698

3 files changed

Lines changed: 31 additions & 17 deletions

File tree

src/FlexRender.Core/Loaders/FileResourceLoader.cs

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ private static bool IsValidScheme(ReadOnlySpan<char> scheme)
127127
}
128128

129129
/// <summary>
130-
/// Validates the path for security issues such as path traversal attacks.
130+
/// Validates the path for security issues: path traversal and absolute paths.
131131
/// </summary>
132132
/// <param name="path">The path to validate.</param>
133-
/// <exception cref="ArgumentException">Thrown when path traversal is detected.</exception>
133+
/// <exception cref="ArgumentException">Thrown when the path is absolute or contains traversal sequences.</exception>
134134
private static void ValidatePathSecurity(string path)
135135
{
136136
if (path.Contains(".."))
@@ -139,25 +139,37 @@ private static void ValidatePathSecurity(string path)
139139
$"Invalid path (path traversal detected): {path}",
140140
nameof(path));
141141
}
142+
143+
if (Path.IsPathRooted(path))
144+
{
145+
throw new ArgumentException(
146+
$"Absolute paths are not allowed for security reasons: {path}. Use relative paths resolved against BasePath.",
147+
nameof(path));
148+
}
142149
}
143150

144151
/// <summary>
145-
/// Resolves a relative or absolute path to a full file system path.
152+
/// Resolves a relative path against BasePath and validates the result stays within bounds.
146153
/// </summary>
147-
/// <param name="path">The path to resolve.</param>
154+
/// <param name="path">The relative path to resolve.</param>
148155
/// <returns>The fully resolved absolute path.</returns>
156+
/// <exception cref="ArgumentException">Thrown when the resolved path escapes the base directory.</exception>
149157
private string ResolvePath(string path)
150158
{
151-
if (Path.IsPathRooted(path))
152-
{
153-
return Path.GetFullPath(path);
154-
}
159+
var basePath = !string.IsNullOrEmpty(_options.BasePath)
160+
? Path.GetFullPath(_options.BasePath)
161+
: Path.GetFullPath(".");
155162

156-
if (!string.IsNullOrEmpty(_options.BasePath))
163+
var fullPath = Path.GetFullPath(Path.Combine(basePath, path));
164+
165+
// Ensure the resolved path is still within the base directory
166+
if (!fullPath.StartsWith(basePath, StringComparison.Ordinal))
157167
{
158-
return Path.GetFullPath(Path.Combine(_options.BasePath, path));
168+
throw new ArgumentException(
169+
$"Path '{path}' resolves outside the base directory.",
170+
nameof(path));
159171
}
160172

161-
return Path.GetFullPath(path);
173+
return fullPath;
162174
}
163175
}

tests/FlexRender.Tests/Loaders/FileResourceLoaderTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ public async Task Load_WithValidFile_ReturnsStream()
4848
// Arrange
4949
var expectedContent = "Hello, World!";
5050
var filePath = CreateTempFile(expectedContent);
51-
var loader = CreateLoader();
51+
var loader = CreateLoader(Path.GetTempPath());
5252

5353
// Act
54-
var stream = await loader.Load(filePath);
54+
var stream = await loader.Load(Path.GetFileName(filePath));
5555

5656
// Assert
5757
stream.Should().NotBeNull();
@@ -67,8 +67,8 @@ public async Task Load_WithValidFile_ReturnsStream()
6767
public async Task Load_WithNonExistentFile_ThrowsFileNotFoundException()
6868
{
6969
// Arrange
70-
var loader = CreateLoader();
71-
var nonExistentPath = Path.Combine(Path.GetTempPath(), "nonexistent_file_12345.txt");
70+
var loader = CreateLoader(Path.GetTempPath());
71+
var nonExistentPath = "nonexistent_file_12345.txt";
7272

7373
// Act
7474
var act = () => loader.Load(nonExistentPath);

tests/FlexRender.Tests/Loaders/ImageLoaderTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class ImageLoaderTests : IDisposable
2525
/// <param name="width">Image width.</param>
2626
/// <param name="height">Image height.</param>
2727
/// <param name="color">Fill color.</param>
28-
/// <returns>Path to the temporary file.</returns>
28+
/// <returns>The filename (relative) of the temporary file within the temp directory.</returns>
2929
private string CreateTestImage(int width, int height, SKColor color)
3030
{
3131
var tempPath = Path.GetTempFileName();
@@ -41,7 +41,7 @@ private string CreateTestImage(int width, int height, SKColor color)
4141
_tempFiles.Add(tempPath);
4242
_tempFiles.Add(pngPath);
4343

44-
return pngPath;
44+
return Path.GetFileName(pngPath);
4545
}
4646

4747
/// <summary>
@@ -53,6 +53,7 @@ private string CreateTestImage(int width, int height, SKColor color)
5353
private static ImageLoader CreateImageLoader(FlexRenderOptions? options = null, params IResourceLoader[] loaders)
5454
{
5555
options ??= new FlexRenderOptions();
56+
options.BasePath ??= Path.GetTempPath();
5657
return new ImageLoader(loaders, options);
5758
}
5859

@@ -64,6 +65,7 @@ private static ImageLoader CreateImageLoader(FlexRenderOptions? options = null,
6465
private static ImageLoader CreateDefaultImageLoader(FlexRenderOptions? options = null)
6566
{
6667
options ??= new FlexRenderOptions();
68+
options.BasePath ??= Path.GetTempPath();
6769
var fileLoader = new FileResourceLoader(options);
6870
var base64Loader = new Base64ResourceLoader(options);
6971
return new ImageLoader([fileLoader, base64Loader], options);

0 commit comments

Comments
 (0)