Skip to content

Commit

Permalink
Sanitize user input
Browse files Browse the repository at this point in the history
A malicious user may be able to forge new log entries or may access
unexpected resources.
  • Loading branch information
flenny committed May 3, 2024
1 parent d9072d1 commit 2d44e45
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 12 deletions.
14 changes: 7 additions & 7 deletions src/Geopilot.Api/Controllers/MandateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ public MandateController(ILogger<MandateController> logger, Context context, IVa
[SwaggerResponse(StatusCodes.Status200OK, "Returns list of mandates associated to the current user matching the optional filter criteria.", typeof(IEnumerable<DeliveryMandate>), new[] { "application/json" })]
public async Task<IActionResult> Get(
[FromQuery, SwaggerParameter("Filter mandates matching validation job file extension.")]
string jobId = "")
Guid jobId = default)
{
logger.LogInformation("Getting mandates for job with id <{JobId}>.", jobId.ReplaceLineEndings(string.Empty));
logger.LogInformation("Getting mandates for job with id <{JobId}>.", jobId);

var user = await context.GetUserByPrincipalAsync(User);
if (user == null)
Expand All @@ -53,24 +53,24 @@ public async Task<IActionResult> Get(
var mandates = context.DeliveryMandates
.Where(m => m.Organisations.SelectMany(o => o.Users).Any(u => u.Id == user.Id));

if (Guid.TryParse(jobId, out var guid))
if (jobId != default)
{
var job = validationService.GetJob(guid);
var job = validationService.GetJob(jobId);
if (job is null)
{
logger.LogTrace("Validation job with id <{JobId}> was not found.", guid.ToString());
logger.LogTrace("Validation job with id <{JobId}> was not found.", jobId);
return Ok(Array.Empty<DeliveryMandate>());
}

logger.LogTrace("Filtering mandates for job with id <{JobId}>", guid.ToString());
logger.LogTrace("Filtering mandates for job with id <{JobId}>", jobId);
var extension = Path.GetExtension(job.OriginalFileName);
mandates = mandates
.Where(m => m.FileTypes.Contains(".*") || m.FileTypes.Contains(extension));
}

var result = await mandates.ToListAsync();

logger.LogInformation("Getting mandates with for job with id <{JobId}> resulted in <{MatchingMandatesCount}> matching mandates.", guid.ToString(), result.Count);
logger.LogInformation("Getting mandates with for job with id <{JobId}> resulted in <{MatchingMandatesCount}> matching mandates.", jobId, result.Count);
return Ok(mandates);
}
}
4 changes: 2 additions & 2 deletions src/Geopilot.Api/Controllers/ValidationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,9 @@ public IActionResult GetStatus(Guid jobId)
[SwaggerResponse(StatusCodes.Status404NotFound, "The job or log file cannot be found.", typeof(ProblemDetails), new[] { "application/json" })]
public IActionResult Download(Guid jobId, string file)
{
var sanitizedFilename = Path.GetFileName(file.Trim().ReplaceLineEndings(string.Empty));
var sanitizedFilename = file.SanitizeFileName();

logger.LogInformation("Download file <{File}> for job <{JobId}> requested.", sanitizedFilename, jobId.ToString());
logger.LogInformation("Download file <{File}> for job <{JobId}> requested.", sanitizedFilename, jobId);

Check failure

Code scanning / CodeQL

Log entries created from user input High

This log entry depends on a
user-provided value
.
fileProvider.Initialize(jobId);

var validationJob = validationService.GetJob(jobId);
Expand Down
30 changes: 30 additions & 0 deletions src/Geopilot.Api/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Globalization;

namespace Geopilot.Api;

/// <summary>
/// GeoPilot API extensions.
/// </summary>
public static class Extensions
{
/// <summary>
/// Sanitizes a file name by removing invalid characters.
/// </summary>
/// <param name="fileName">The file name to sanitize.</param>
/// <returns>The sanitized file name.</returns>
public static string SanitizeFileName(this string fileName)
{
fileName = fileName.Trim().ReplaceLineEndings(string.Empty);

// Get invalid characters for file names and add some platform-specific ones.
var invalidFileNameChars = Path.GetInvalidFileNameChars()
.Concat(new[] { '?', '$', '*', '|', '<', '>', '"', ':' }).ToArray();

foreach (var invalidFileNameChar in invalidFileNameChars)
{
fileName = fileName.Replace(invalidFileNameChar.ToString(CultureInfo.InvariantCulture), string.Empty);
}

return fileName;
}
}
6 changes: 3 additions & 3 deletions tests/Geopilot.Api.Test/Controllers/MandateControllerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public async Task GetWithJobIdIncludesMatchingMandates()
.Setup(m => m.GetJob(jobId))
.Returns(new ValidationJob(jobId, "Original.xtf", "tmp.xtf"));

var result = (await mandateController.Get(jobId.ToString())) as OkObjectResult;
var result = (await mandateController.Get(jobId)) as OkObjectResult;
var mandates = (result?.Value as IEnumerable<DeliveryMandate>)?.ToList();

Assert.IsNotNull(mandates);
Expand All @@ -88,7 +88,7 @@ public async Task GetWithJobIdExcludesNonMatchinMandates()
.Setup(m => m.GetJob(jobId))
.Returns(new ValidationJob(jobId, "Original.csv", "tmp.csv"));

var result = (await mandateController.Get(jobId: jobId.ToString())) as OkObjectResult;
var result = (await mandateController.Get(jobId)) as OkObjectResult;
var mandates = (result?.Value as IEnumerable<DeliveryMandate>)?.ToList();

Assert.IsNotNull(mandates);
Expand All @@ -106,7 +106,7 @@ public async Task GetWithInvalidJobIdReturnsEmptyArray()
.Setup(m => m.GetJob(jobId))
.Returns(() => null);

var result = (await mandateController.Get(jobId: jobId.ToString())) as OkObjectResult;
var result = (await mandateController.Get(jobId)) as OkObjectResult;
var mandates = (result?.Value as IEnumerable<DeliveryMandate>)?.ToList();

Assert.IsNotNull(mandates);
Expand Down
37 changes: 37 additions & 0 deletions tests/Geopilot.Api.Test/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
namespace Geopilot.Api.Controllers;

[TestClass]
public class Extensions
{
[TestMethod]
public void SanitizeFileNameForValidFileNames()
{
AssertSanitizeFileName("SQUIRRELGENESIS", "SQUIRRELGENESIS");
AssertSanitizeFileName("JUNIORARK.xyz", "JUNIORARK.xyz");
AssertSanitizeFileName("PEEVEDBEAM-ANT.MESS.abc", "PEEVEDBEAM-ANT.MESS.abc");
AssertSanitizeFileName("WEIRD WATER.example", "WEIRD WATER.example");
AssertSanitizeFileName("AUTOFIRE123.doc", "AUTOFIRE123.doc");
AssertSanitizeFileName("SUNNY(1).doc", "SUNNY(1).doc");
AssertSanitizeFileName("ODD_MONKEY.doc", "ODD_MONKEY.doc");
AssertSanitizeFileName("SILLY,MONKEY.docx", "SILLY,MONKEY.docx");
AssertSanitizeFileName("CamelCase.bat", "CamelCase.bat");
AssertSanitizeFileName("SLICKER-CHIPMUNK.bat", "SLICKER-CHIPMUNK.bat");
}

[TestMethod]
public void SanitizeFileNameForInvalidFileNames()
{
AssertSanitizeFileName("CHIPMUNKWALK", " CHIPMUNKWALK ");
AssertSanitizeFileName("SLEEPYBOUNCE", "SLEEPYBOUNCE\n");
AssertSanitizeFileName("PLOWARK", "PLOWARK\r");
AssertSanitizeFileName("JUNIORGLEE", "JUNIORGLEE\t");
AssertSanitizeFileName("LATENTROUTE34", "LATENTROUTE?34");
AssertSanitizeFileName("TRAWLSOUFFLE", "TRAWLSOUFFLE*");
AssertSanitizeFileName("VIOLENTIRON", "><VIOLENTIRON\"|");
AssertSanitizeFileName("YELLOWBAGEL", "YELLOWBAGEL://");
AssertSanitizeFileName("ZANYWATER", "ZANYWATER$");
}

private static void AssertSanitizeFileName(string expected, string fileName)
=> Assert.AreEqual(expected, fileName.SanitizeFileName());
}

0 comments on commit 2d44e45

Please sign in to comment.