mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-06-27 19:36:28 +00:00
Adding security features
This commit is contained in:
parent
b55663ef62
commit
84a9f88f8b
@ -675,13 +675,14 @@ public abstract class BaseProvider : IProvider, ISecretId
|
||||
Content = responseMessage.Content,
|
||||
ToolCalls = responseMessage.ToolCalls,
|
||||
});
|
||||
|
||||
|
||||
var maxToolCalls = 30;
|
||||
foreach (var toolCall in responseMessage.ToolCalls)
|
||||
{
|
||||
toolCallCount++;
|
||||
if (toolCallCount > 10)
|
||||
if (toolCallCount > maxToolCalls)
|
||||
{
|
||||
var limitMessage = "Tool calling stopped because the maximum of 10 tool calls was reached.";
|
||||
var limitMessage = $"Tool calling stopped because the maximum of {maxToolCalls} tool calls was reached.";
|
||||
currentAssistantContent.ToolInvocations.Add(new ToolInvocationTrace
|
||||
{
|
||||
Order = toolCallCount,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using HtmlAgilityPack;
|
||||
using ReverseMarkdown;
|
||||
|
||||
@ -10,6 +11,7 @@ public sealed class HTMLParser
|
||||
{
|
||||
private const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) MindWorkAIStudio/1.0";
|
||||
private const int MAX_REDIRECTS = 10;
|
||||
private const int DEFAULT_MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
private static readonly Config MARKDOWN_PARSER_CONFIG = new()
|
||||
{
|
||||
@ -42,7 +44,7 @@ public sealed class HTMLParser
|
||||
return innerHtml;
|
||||
}
|
||||
|
||||
public async Task<HTMLParserWebPage> LoadWebPageAsync(Uri url, CancellationToken token = default, int timeoutSeconds = 30, Func<Uri, CancellationToken, Task<IReadOnlyList<IPAddress>>>? resolveUrlAddressesAsync = null)
|
||||
public async Task<HTMLParserWebPage> LoadWebPageAsync(Uri url, CancellationToken token = default, int timeoutSeconds = 30, Func<Uri, CancellationToken, Task<IReadOnlyList<IPAddress>>>? resolveUrlAddressesAsync = null, int maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES)
|
||||
{
|
||||
using var handler = new SocketsHttpHandler
|
||||
{
|
||||
@ -89,7 +91,7 @@ public sealed class HTMLParser
|
||||
throw new HttpRequestException($"The server returned HTTP {statusCode} ({reasonPhrase}) for '{currentUrl}'.", null, response.StatusCode);
|
||||
}
|
||||
|
||||
var html = await response.Content.ReadAsStringAsync(timeoutCts.Token);
|
||||
var html = await ReadContentAsStringWithLimitAsync(response.Content, maxResponseBytes, timeoutCts.Token);
|
||||
var document = new HtmlDocument();
|
||||
document.LoadHtml(html);
|
||||
|
||||
@ -178,6 +180,46 @@ public sealed class HTMLParser
|
||||
|
||||
private static bool IsRedirect(HttpStatusCode statusCode) => (int)statusCode is >= 300 and <= 399;
|
||||
|
||||
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
await using var stream = await content.ReadAsStreamAsync(token);
|
||||
await using var buffer = new MemoryStream();
|
||||
var chunk = new byte[8192];
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(chunk, token);
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
if (buffer.Length + read > maxResponseBytes)
|
||||
throw new HttpRequestException($"The response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
buffer.Write(chunk, 0, read);
|
||||
}
|
||||
|
||||
var encoding = TryGetContentEncoding(content) ?? Encoding.UTF8;
|
||||
return encoding.GetString(buffer.ToArray());
|
||||
}
|
||||
|
||||
private static Encoding? TryGetContentEncoding(HttpContent content)
|
||||
{
|
||||
var charset = content.Headers.ContentType?.CharSet?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(charset))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return Encoding.GetEncoding(charset.Trim('"'));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string ExtractTitle(HtmlDocument document)
|
||||
{
|
||||
var title = document.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();
|
||||
|
||||
@ -14,6 +14,9 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
||||
|
||||
private const int DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
private const int DEFAULT_MAX_CONTENT_CHARACTERS = 12000;
|
||||
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||
private const int MAX_CONTENT_CHARACTERS = 50000;
|
||||
private const int MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
||||
private const int MAX_TRACE_LENGTH = 12000;
|
||||
private const string ALLOWED_PRIVATE_HOSTS_SETTING = "allowedPrivateHosts";
|
||||
|
||||
@ -99,8 +102,8 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
||||
if (!Uri.TryCreate(urlText, UriKind.Absolute, out var url) || url is not { Scheme: "http" or "https" })
|
||||
throw new ArgumentException("Argument 'url' must be a valid HTTP or HTTPS URL.");
|
||||
|
||||
var timeoutSeconds = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS;
|
||||
var maxContentCharacters = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS;
|
||||
var timeoutSeconds = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||
var maxContentCharacters = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "maxContentCharacters") ?? DEFAULT_MAX_CONTENT_CHARACTERS, MAX_CONTENT_CHARACTERS);
|
||||
if (!TryReadAllowedPrivateHostPatterns(context.SettingsValues.GetValueOrDefault(ALLOWED_PRIVATE_HOSTS_SETTING), out var allowedPrivateHosts, out var allowlistError))
|
||||
throw new InvalidOperationException(allowlistError);
|
||||
|
||||
@ -111,7 +114,8 @@ public sealed class ReadWebPageTool(HTMLParser htmlParser, ILogger<ReadWebPageTo
|
||||
url,
|
||||
token,
|
||||
timeoutSeconds,
|
||||
async (candidateUrl, validationToken) => await this.ResolveValidatedUrlAddressesAsync(candidateUrl, allowedPrivateHosts, context.ProviderConfidence, validationToken));
|
||||
async (candidateUrl, validationToken) => await this.ResolveValidatedUrlAddressesAsync(candidateUrl, allowedPrivateHosts, context.ProviderConfidence, validationToken),
|
||||
MAX_RESPONSE_BYTES);
|
||||
}
|
||||
catch (OperationCanceledException) when (!token.IsCancellationRequested)
|
||||
{
|
||||
|
||||
@ -12,6 +12,10 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
||||
|
||||
private const int DEFAULT_MAX_RESULTS = 5;
|
||||
private const int DEFAULT_TIMEOUT_SECONDS = 20;
|
||||
private const int MAX_RESULTS = 20;
|
||||
private const int MAX_PAGE = 20;
|
||||
private const int MAX_TIMEOUT_SECONDS = 60;
|
||||
private const int MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
private const int MAX_TRACE_LENGTH = 4000;
|
||||
|
||||
public string ImplementationKey => "web_search";
|
||||
@ -127,8 +131,10 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
||||
throw new InvalidOperationException(TB("Default categories and default engines cannot both be set for the web search tool."));
|
||||
|
||||
var defaultLimit = ReadOptionalPositiveIntSetting(context.SettingsValues, "maxResults") ?? DEFAULT_MAX_RESULTS;
|
||||
var effectiveLimit = requestedLimit ?? defaultLimit;
|
||||
var timeoutSeconds = ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS;
|
||||
var effectiveLimit = Math.Min(requestedLimit ?? defaultLimit, MAX_RESULTS);
|
||||
var timeoutSeconds = Math.Min(ReadOptionalPositiveIntSetting(context.SettingsValues, "timeoutSeconds") ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
||||
if (page is > MAX_PAGE)
|
||||
throw new ArgumentException($"Argument 'page' must be less than or equal to {MAX_PAGE}.");
|
||||
|
||||
var queryParameters = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
@ -163,7 +169,7 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
||||
timeoutCts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||
|
||||
using var response = await SendAsync(httpClient, request, timeoutCts.Token, timeoutSeconds, token);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(token);
|
||||
var responseBody = await ReadContentAsStringWithLimitAsync(response.Content, MAX_RESPONSE_BYTES, token);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseDetails = string.IsNullOrWhiteSpace(responseBody) ? string.Empty : $" Response body: {responseBody[..Math.Min(responseBody.Length, 400)]}";
|
||||
@ -409,6 +415,29 @@ public sealed class SearXNGWebSearchTool : IToolImplementation
|
||||
return int.TryParse(value, out var parsedValue) && parsedValue > 0 ? parsedValue : null;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadContentAsStringWithLimitAsync(HttpContent content, int maxResponseBytes, CancellationToken token)
|
||||
{
|
||||
if (content.Headers.ContentLength is long contentLength && contentLength > maxResponseBytes)
|
||||
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
await using var stream = await content.ReadAsStreamAsync(token);
|
||||
await using var buffer = new MemoryStream();
|
||||
var chunk = new byte[8192];
|
||||
while (true)
|
||||
{
|
||||
var read = await stream.ReadAsync(chunk, token);
|
||||
if (read == 0)
|
||||
break;
|
||||
|
||||
if (buffer.Length + read > maxResponseBytes)
|
||||
throw new InvalidOperationException($"The SearXNG response body is too large. Maximum allowed size is {maxResponseBytes} bytes.");
|
||||
|
||||
buffer.Write(chunk, 0, read);
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(buffer.ToArray());
|
||||
}
|
||||
|
||||
private static bool TryReadOptionalPositiveInt(
|
||||
IReadOnlyDictionary<string, string> settingsValues,
|
||||
string key,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user