Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Services/appconfig/aiservice/appsettings.dev.json.template
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
},
"GapAnalysisProcessing": {
"processwatcherUrl": "{{ gapanalysisprocesswatcherurl }}"
},
"AntiSSRF": {
"AllowedDomains": "microsoft.seismic.com"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont add anu value here. We cannot determine the URL used by users

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dont add anu value here. We cannot determine the URL used by users

}
}

Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public static void UseESRSEndpoint(this WebApplication app)
var content = new StringContent(serializedParamJson, Encoding.UTF8, "application/json");

//Invoke Process Watcher
var response = await appContext.httpClient.PostAsync(config["GapAnalysisProcessing:processwatcherUrl"], content);
var response = await appContext.httpClient.PostAsync(config["GapAnalysisProcessing:processwatcherUrl"], content); // CodeQL [SM03781] We are reading this value from appsettings.json, this is not an user input

return Results.Accepted(locationUrl, gapAnalysisServiceRequest);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ await this.memoryWebClient.ImportDocumentAsync(content: fileStream.BaseStream,

async public Task<DocumentServiceResult> RegisterDocumentFromFileUrl(RegisterDocumentFromFileUrlServiceRequest serviceRequest)
{
//Validate URL to prevent SSRF attack
if (AntiSsrfValidation(serviceRequest.FileUrl))
{
throw new Exception("AntiSSRF validation failed - Invalid or unauthorized URL provided");
}
//Download file from URL then take only fileName from URL
//the file location URL in the document will be mixed with SAS token to get it.
//sample url - https://microsoft.seismic.com/app?ContentId=d6e9f9bb-70d4-4845-a2ad-dd25ecc343d6#/doccenter/a5266a70-9230-4c1e-a553-c5bddcb7a896/doc/%252Fdde0caec0e-9236-f21b-2991-5868e63d3984%252FdfYTZjNDRiZDMtMzEwZS1kNWZkLTNjOGEtNjliYWJjMjhmMmUw%252CPT0%253D%252CUGl0Y2ggRGVjaw%253D%253D%252Flffb13c1f1-d960-4bbe-8685-000afbf5a67f//?mode=view&parentPath=sessionStorage
Expand Down Expand Up @@ -303,6 +308,49 @@ async public Task<MemoryAnswer> AskAboutDocumentSummary(string DocumentId, strin
return await this.memoryWebClient.AskAsync(Question, filter: new MemoryFilter().ByDocument(DocumentId));
}
}

// Anti-SSRF validation method
private bool AntiSsrfValidation(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return true; // Invalid URL
}

if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri))
{
return true; // Invalid URL format
}

//the parameter flows to the validation method
bool isInvalidUri = !IsInAllowedDomain(uri) || !IsHttpsScheme(uri);

//validation method call flows to boolean return statement
return isInvalidUri;
}

//Domain validation helper
private bool IsInAllowedDomain(Uri uri)
{
var allowedDomainsConfig = this.config["AntiSSRF:AllowedDomains"];
if (string.IsNullOrWhiteSpace(allowedDomainsConfig))
{
throw new InvalidOperationException("AllowedDomains configuration is missing");
}
// Add this line to parse the config string into an array
var allowedDomains = allowedDomainsConfig.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(d => d.Trim())
.ToArray();

return allowedDomains.Any(domain =>
uri.Host.Equals(domain, StringComparison.OrdinalIgnoreCase) ||
uri.Host.EndsWith($".{domain}", StringComparison.OrdinalIgnoreCase));
}

// HTTPS validation helper
private bool IsHttpsScheme(Uri uri)
{
return uri.Scheme == Uri.UriSchemeHttps;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using CFS.SK.Sustainability.AI.Storage.GapAnalysis;
using Microsoft.Identity.Client;
using Azure.Identity;
using System.Text.RegularExpressions;


namespace CFS.SK.Sustainability.AI
Expand All @@ -35,6 +36,10 @@
private readonly GapAnalysisJobRepository _gapAnalysisJobRepository;
private readonly IConfiguration _config;

// Regex to validate file names (only allows letters, digits, dot, dash, underscore)
private static readonly Regex ValidFileNameRegex =
new Regex(@"^[A-Za-z0-9._\-]+$", RegexOptions.Compiled);

public ESRSGapAnalysisManager(ApplicationContext appContext,
ILogger<ESRSGapAnalysisManager> logger,
ESRSDisclosureRetriever esrsDisclosureRetriever,
Expand Down Expand Up @@ -196,7 +201,13 @@
//Create BlobClient
//Get Current YYYYMMDDHHMMSS
var jobId = DateTime.Now.ToString("yyyyMMddHHmmss");
var fileName = $"GAPAnalysisReport-{disclosure_number}-{jobId}";
// Sanitize disclosure_number to remove invalid characters
var sanitizedDisclosureNumber = new string(disclosure_number
.Where(c => char.IsLetterOrDigit(c) || c == '-' || c == '_')
.ToArray());
var fileName = $"GAPAnalysisReport-{sanitizedDisclosureNumber}-{jobId}";
//validate file name
EnsureSafeSimpleFileName(fileName);
var blobClient = blobContainerClient.GetBlobClient($"{fileName}.md");
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(analysis_resultString)))
{
Expand All @@ -209,6 +220,8 @@

//Create BlobClient
var htmlFileName = $"{fileName}.html";
//validate html file name
EnsureSafeSimpleFileName(htmlFileName);
blobClient = blobContainerClient.GetBlobClient(htmlFileName);
byte[] byteArray_html = MarkdownHtmlConverter.Convert(analysis_resultString);

Expand All @@ -226,6 +239,7 @@
//Convert HTML to PDF
//Need to extra work for convert html to pdf
var pdfFileName = $"{fileName}.pdf";
EnsureSafeSimpleFileName(pdfFileName); // Validate pdffilename
HtmlPdfConverter.Convert(htmlFileName, pdfFileName);

//if Pdf file is exist then upload to the blob
Expand All @@ -236,7 +250,7 @@
blobClient = blobContainerClient.GetBlobClient(pdfFileName);
using (Stream stream = new FileStream(pdfFileName, FileMode.Open))
{
await blobClient.UploadAsync(stream, true);
};

//Client SAS Token Url for the file
Expand All @@ -245,7 +259,7 @@
//Delete pdf file
System.IO.File.Delete(pdfFileName);
//Delete html file
System.IO.File.Delete(htmlFileName);
System.IO.File.Delete(htmlFileName);// CodeQL [SM00414] This variable is not based on user input, so no need to handle the Code QL issue.
}
else
{
Expand All @@ -254,7 +268,8 @@
throw new Exception("PDF File is not converted");
}

var metaDataFileName = $"GAPAnalysisReport-{disclosure_number}-{jobId}-meta.json";
var metaDataFileName = $"GAPAnalysisReport-{sanitizedDisclosureNumber}-{jobId}-meta.json";
EnsureSafeSimpleFileName(metaDataFileName);// Validate metadatafilename
blobClient = blobContainerClient.GetBlobClient(metaDataFileName);

using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(disclosureDescription))))
Expand Down Expand Up @@ -293,5 +308,20 @@
return gapAnalysis_response;
//return result.GetValue<string>();
}

// Validate simple file name to prevent path traversal attacks
private static void EnsureSafeSimpleFileName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("File name is empty or null.");

if (name.Contains("..") || name.Contains("/") || name.Contains("\\"))
throw new ArgumentException("Invalid file name (contains path components or traversal).");

// Whitelist chars: letters, digits, dash, underscore, dot
if (!ValidFileNameRegex.IsMatch(name))
throw new ArgumentException("Invalid file name.");

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,28 @@ internal static class HtmlPdfConverter
{
public static bool Convert(string sourceHtmlFilePath, string targetPdfFilePath)
{
var escapedSourceHtmlFilePath = sourceHtmlFilePath.Replace("\"", "\\\"");
var escapedTargetPdfFilePath = targetPdfFilePath.Replace("\"", "\\\"");
// Validate file paths to prevent command injection
ValidateFilePath(sourceHtmlFilePath);
ValidateFilePath(targetPdfFilePath);

var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = IsWindows() ? "wkhtmltopdf.exe" : "/usr/bin/wkhtmltopdf",
Arguments = $"--encoding UTF-8 -q \"{escapedSourceHtmlFilePath}\" \"{escapedTargetPdfFilePath}\"",
RedirectStandardInput = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};
// Use ArgumentList to safely pass arguments (prevents command injection)
process.StartInfo.ArgumentList.Add("--encoding");
process.StartInfo.ArgumentList.Add("UTF-8");
process.StartInfo.ArgumentList.Add("-q");
process.StartInfo.ArgumentList.Add(sourceHtmlFilePath);
process.StartInfo.ArgumentList.Add(targetPdfFilePath);

process.ErrorDataReceived += (process, data) =>
{
Expand All @@ -54,5 +61,16 @@ private static bool IsWindows()
{
return System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
}

// Simple validation to prevent command injection
private static void ValidateFilePath(string path)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentException("Invalid path");

// Block command injection characters
if (path.Any(c => ";|&`$<>\n\r".Contains(c)))
throw new ArgumentException("Path contains dangerous characters");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ public async Task CreateTableAsync(
CancellationToken cancellationToken = default)
{
var origInputTableName = tableName;

// Validate tableName parameter before using it in SQL construction
PostgresSchema.ValidateTableName(origInputTableName);

tableName = this.WithSchemaAndTableNamePrefix(tableName);
this._log.LogTrace("Creating table: {0}", tableName);

Expand All @@ -154,12 +158,12 @@ public async Task CreateTableAsync(
using NpgsqlCommand cmd = connection.CreateCommand();

var lockId = GenLockId(tableName);

#pragma warning disable CA2100 // SQL reviewed
#pragma warning disable CA2100 // SQL reviewed
if (!string.IsNullOrEmpty(this._createTableSql))
{
cmd.CommandText = this._createTableSql
.Replace(PostgresConfig.SqlPlaceholdersTableName, tableName, StringComparison.Ordinal)
.Replace(PostgresConfig.SqlPlaceholdersTableName, tableName, StringComparison.Ordinal) // CodeQL [SM03934] tableName parameter is validated by PostgresSchema.ValidateTableName to prevent SQL injection
.Replace(PostgresConfig.SqlPlaceholdersVectorSize, $"{vectorSize}", StringComparison.Ordinal)
.Replace(PostgresConfig.SqlPlaceholdersLockId, $"{lockId}", StringComparison.Ordinal);

Expand Down
Loading