Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4945b9b

Browse files
committed
Merge remote-tracking branch 'origin/main'
# Conflicts: # .github/workflows/dotnet.yml
2 parents c9cbd6e + 7ea191a commit 4945b9b

File tree

21 files changed

+872
-66
lines changed

21 files changed

+872
-66
lines changed

Integraions/ManagedCode.Storage.Server/ChunkUpload/ChunkUploadService.cs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ public async Task<Result> AppendChunkAsync(FileUploadPayload payload, Cancellati
4747
var descriptor = payload.Payload;
4848
var uploadId = ChunkUploadDescriptor.ResolveUploadId(descriptor);
4949

50-
var session = _sessions.GetOrAdd(uploadId, static (key, state) =>
50+
// Sanitize upload ID to prevent path traversal
51+
var sanitizedUploadId = SanitizeUploadId(uploadId);
52+
53+
var session = _sessions.GetOrAdd(sanitizedUploadId, static (key, state) =>
5154
{
5255
var descriptor = state.Payload;
5356
var workingDirectory = Path.Combine(state.Options.TempPath, key);
@@ -151,6 +154,29 @@ public void Abort(string uploadId)
151154
}
152155
}
153156

157+
private static string SanitizeUploadId(string uploadId)
158+
{
159+
if (string.IsNullOrWhiteSpace(uploadId))
160+
throw new ArgumentException("Upload ID cannot be null or empty", nameof(uploadId));
161+
162+
// Remove any path traversal sequences
163+
uploadId = uploadId.Replace("..", string.Empty, StringComparison.Ordinal);
164+
uploadId = uploadId.Replace("/", string.Empty, StringComparison.Ordinal);
165+
uploadId = uploadId.Replace("\\", string.Empty, StringComparison.Ordinal);
166+
167+
// Only allow alphanumeric characters, hyphens, and underscores
168+
var allowedChars = uploadId.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_').ToArray();
169+
var sanitized = new string(allowedChars);
170+
171+
if (string.IsNullOrWhiteSpace(sanitized))
172+
throw new ArgumentException("Upload ID contains only invalid characters", nameof(uploadId));
173+
174+
if (sanitized.Length > 128)
175+
throw new ArgumentException("Upload ID is too long", nameof(uploadId));
176+
177+
return sanitized;
178+
}
179+
154180
private static async Task MergeChunksAsync(string destinationFile, IReadOnlyCollection<string> chunkFiles, CancellationToken cancellationToken)
155181
{
156182
await using var destination = new FileStream(destinationFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: MergeBufferSize, useAsync: true);

Integraions/ManagedCode.Storage.Server/Controllers/StorageControllerBase.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ public virtual async Task<Result<BlobMetadata>> UploadAsync([FromForm] IFormFile
5757
return Result<BlobMetadata>.Fail(HttpStatusCode.BadRequest, "File payload is missing");
5858
}
5959

60+
// Validate file size if enabled
61+
if (_options.EnableFileSizeValidation && _options.MaxFileSize > 0 && file.Length > _options.MaxFileSize)
62+
{
63+
return Result<BlobMetadata>.Fail(HttpStatusCode.RequestEntityTooLarge,
64+
$"File size {file.Length} bytes exceeds maximum allowed size of {_options.MaxFileSize} bytes");
65+
}
66+
6067
try
6168
{
6269
return await Result.From(() => this.UploadFormFileAsync(Storage, file, cancellationToken: cancellationToken), cancellationToken);
@@ -164,6 +171,13 @@ public virtual async Task<Result> UploadChunkAsync([FromForm] FileUploadPayload
164171
return Result.Fail(HttpStatusCode.BadRequest, "UploadId is required");
165172
}
166173

174+
// Validate chunk size if enabled
175+
if (_options.EnableFileSizeValidation && _options.MaxChunkSize > 0 && payload.File.Length > _options.MaxChunkSize)
176+
{
177+
return Result.Fail(HttpStatusCode.RequestEntityTooLarge,
178+
$"Chunk size {payload.File.Length} bytes exceeds maximum allowed chunk size of {_options.MaxChunkSize} bytes");
179+
}
180+
167181
return await ChunkUploadService.AppendChunkAsync(payload, cancellationToken);
168182
}
169183

@@ -228,6 +242,16 @@ public class StorageServerOptions
228242
/// </summary>
229243
public const int DefaultMultipartBoundaryLengthLimit = 70;
230244

245+
/// <summary>
246+
/// Default maximum file size: 100 MB.
247+
/// </summary>
248+
public const long DefaultMaxFileSize = 100 * 1024 * 1024;
249+
250+
/// <summary>
251+
/// Default maximum chunk size: 10 MB.
252+
/// </summary>
253+
public const long DefaultMaxChunkSize = 10 * 1024 * 1024;
254+
231255
/// <summary>
232256
/// Gets or sets a value indicating whether range processing is enabled for streaming responses.
233257
/// </summary>
@@ -242,4 +266,22 @@ public class StorageServerOptions
242266
/// Gets or sets the maximum allowed length for multipart boundaries when parsing raw upload streams.
243267
/// </summary>
244268
public int MultipartBoundaryLengthLimit { get; set; } = DefaultMultipartBoundaryLengthLimit;
269+
270+
/// <summary>
271+
/// Gets or sets the maximum file size in bytes that can be uploaded. Set to 0 to disable the limit.
272+
/// Default is 100 MB.
273+
/// </summary>
274+
public long MaxFileSize { get; set; } = DefaultMaxFileSize;
275+
276+
/// <summary>
277+
/// Gets or sets the maximum chunk size in bytes for chunk uploads. Set to 0 to disable the limit.
278+
/// Default is 10 MB.
279+
/// </summary>
280+
public long MaxChunkSize { get; set; } = DefaultMaxChunkSize;
281+
282+
/// <summary>
283+
/// Gets or sets whether file size validation is enabled.
284+
/// Default is true.
285+
/// </summary>
286+
public bool EnableFileSizeValidation { get; set; } = true;
245287
}

ManagedCode.Storage.Core/Models/LocalFile.cs

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,11 @@ public void Close()
131131

132132
public async Task<LocalFile> CopyFromStreamAsync(Stream stream, CancellationToken cancellationToken = default)
133133
{
134-
var fs = FileStream;
135-
await stream.CopyToAsync(fs, cancellationToken);
134+
await using var fs = FileStream;
135+
fs.SetLength(0);
136+
fs.Position = 0;
137+
await stream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
138+
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
136139
return this;
137140
}
138141

@@ -180,6 +183,11 @@ public static LocalFile FromTempFile()
180183
return new LocalFile();
181184
}
182185

186+
public Stream OpenReadStream(bool disposeOwner = true)
187+
{
188+
return new LocalFileReadStream(this, disposeOwner);
189+
}
190+
183191
#region Read
184192

185193
public string ReadAllText()
@@ -304,4 +312,136 @@ public Task WriteAllBytesAsync(byte[] bytes, CancellationToken cancellationToken
304312
}
305313

306314
#endregion
307-
}
315+
316+
private sealed class LocalFileReadStream : Stream
317+
{
318+
private readonly LocalFile _owner;
319+
private readonly FileStream _stream;
320+
private readonly bool _disposeOwner;
321+
private bool _disposed;
322+
323+
public LocalFileReadStream(LocalFile owner, bool disposeOwner)
324+
{
325+
_owner = owner;
326+
_disposeOwner = disposeOwner;
327+
_stream = new FileStream(owner.FilePath, new FileStreamOptions
328+
{
329+
Mode = FileMode.Open,
330+
Access = FileAccess.Read,
331+
Share = FileShare.Read,
332+
Options = FileOptions.Asynchronous
333+
});
334+
}
335+
336+
public override bool CanRead => _stream.CanRead;
337+
338+
public override bool CanSeek => _stream.CanSeek;
339+
340+
public override bool CanWrite => _stream.CanWrite;
341+
342+
public override long Length => _stream.Length;
343+
344+
public override long Position
345+
{
346+
get => _stream.Position;
347+
set => _stream.Position = value;
348+
}
349+
350+
public override void Flush() => _stream.Flush();
351+
352+
public override Task FlushAsync(CancellationToken cancellationToken)
353+
{
354+
return _stream.FlushAsync(cancellationToken);
355+
}
356+
357+
public override int Read(byte[] buffer, int offset, int count)
358+
{
359+
return _stream.Read(buffer, offset, count);
360+
}
361+
362+
public override int Read(Span<byte> buffer)
363+
{
364+
return _stream.Read(buffer);
365+
}
366+
367+
public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
368+
{
369+
return _stream.ReadAsync(buffer, cancellationToken);
370+
}
371+
372+
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
373+
{
374+
return _stream.ReadAsync(buffer, offset, count, cancellationToken);
375+
}
376+
377+
public override long Seek(long offset, SeekOrigin origin)
378+
{
379+
return _stream.Seek(offset, origin);
380+
}
381+
382+
public override void SetLength(long value)
383+
{
384+
_stream.SetLength(value);
385+
}
386+
387+
public override void Write(byte[] buffer, int offset, int count)
388+
{
389+
_stream.Write(buffer, offset, count);
390+
}
391+
392+
public override void Write(ReadOnlySpan<byte> buffer)
393+
{
394+
_stream.Write(buffer);
395+
}
396+
397+
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
398+
{
399+
return _stream.WriteAsync(buffer, offset, count, cancellationToken);
400+
}
401+
402+
public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
403+
{
404+
return _stream.WriteAsync(buffer, cancellationToken);
405+
}
406+
407+
protected override void Dispose(bool disposing)
408+
{
409+
if (_disposed)
410+
{
411+
base.Dispose(disposing);
412+
return;
413+
}
414+
415+
if (disposing)
416+
{
417+
_stream.Dispose();
418+
419+
if (_disposeOwner)
420+
{
421+
_owner.Dispose();
422+
}
423+
}
424+
425+
_disposed = true;
426+
base.Dispose(disposing);
427+
}
428+
429+
public override async ValueTask DisposeAsync()
430+
{
431+
if (_disposed)
432+
{
433+
await ValueTask.CompletedTask;
434+
return;
435+
}
436+
437+
await _stream.DisposeAsync();
438+
439+
if (_disposeOwner)
440+
{
441+
await _owner.DisposeAsync();
442+
}
443+
444+
_disposed = true;
445+
}
446+
}
447+
}

ManagedCode.Storage.VirtualFileSystem/Core/VfsPath.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Linq;
45

56
namespace ManagedCode.Storage.VirtualFileSystem.Core;
67

@@ -95,6 +96,14 @@ public string ToBlobKey()
9596
/// </summary>
9697
private static string NormalizePath(string path)
9798
{
99+
// Security: Check for null bytes (potential security issue)
100+
if (path.Contains('\0'))
101+
throw new ArgumentException("Path contains null bytes", nameof(path));
102+
103+
// Security: Check for control characters
104+
if (path.Any(c => char.IsControl(c) && c != '\t' && c != '\r' && c != '\n'))
105+
throw new ArgumentException("Path contains control characters", nameof(path));
106+
98107
// 1. Replace backslashes with forward slashes
99108
path = path.Replace('\\', '/');
100109

0 commit comments

Comments
 (0)