Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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": ""
}
}

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 @@ public class ESRSGapAnalysisManager : SemanticKernelLogicBase<ESRSGapAnalysisMan
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,15 @@ public async Task<GapAnalysisJob> RegisterJob(GapAnalysisServiceRequest jobReque
//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
fileName = ValidateAndReturnSafeFileName(fileName);
var blobClient = blobContainerClient.GetBlobClient($"{fileName}.md");
using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(analysis_resultString)))
{
Expand All @@ -209,6 +222,8 @@ public async Task<GapAnalysisJob> RegisterJob(GapAnalysisServiceRequest jobReque

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

Expand All @@ -226,6 +241,7 @@ public async Task<GapAnalysisJob> RegisterJob(GapAnalysisServiceRequest jobReque
//Convert HTML to PDF
//Need to extra work for convert html to pdf
var pdfFileName = $"{fileName}.pdf";
pdfFileName = ValidateAndReturnSafeFileName(pdfFileName); // Validate pdffilename
HtmlPdfConverter.Convert(htmlFileName, pdfFileName);

//if Pdf file is exist then upload to the blob
Expand Down Expand Up @@ -254,7 +270,8 @@ public async Task<GapAnalysisJob> RegisterJob(GapAnalysisServiceRequest jobReque
throw new Exception("PDF File is not converted");
}

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

using (Stream stream = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(disclosureDescription))))
Expand Down Expand Up @@ -293,5 +310,22 @@ public async Task<GapAnalysisJob> RegisterJob(GapAnalysisServiceRequest jobReque
return gapAnalysis_response;
//return result.GetValue<string>();
}

// Validate simple file name to prevent path traversal attacks
// Returns the validated filename to help static analysis tools track sanitization
private static string ValidateAndReturnSafeFileName(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.");

return 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