From 6d2a8fc8c90612f826fb502ea7966e57cd2dc811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Peer=20Sch=C3=BCtt?= <20603780+peerschuett@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:06:42 +0100 Subject: [PATCH] Add AdditionalApiParameters input validation and normalization --- .../Dialogs/ProviderDialog.razor | 4 +- .../Dialogs/ProviderDialog.razor.cs | 168 +++++++++++++++++- 2 files changed, 168 insertions(+), 4 deletions(-) diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index 96e94a2f..4c09da2f 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -160,7 +160,7 @@ @T("Please be aware: This section is for experts only. You are responsible for verifying the correctness of the additional parameters you provide to the API call. By default, AI Studio uses the OpenAI-compatible chat completions API, when that it is supported by the underlying service and model.") - + @@ -181,4 +181,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 216f49ee..994c84f5 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -1,3 +1,6 @@ +using System.Text; +using System.Text.Json; + using AIStudio.Components; using AIStudio.Provider; using AIStudio.Provider.HuggingFace; @@ -334,7 +337,168 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId private void OnInputChangeExpertSettings() { - this.AdditionalJsonApiParameters = this.AdditionalJsonApiParameters.Trim().TrimEnd(',', ' '); + this.AdditionalJsonApiParameters = NormalizeAdditionalJsonApiParameters(this.AdditionalJsonApiParameters) + .Trim() + .TrimEnd(',', ' '); + } + + private string? ValidateAdditionalJsonApiParameters(string additionalParams) + { + if (string.IsNullOrWhiteSpace(additionalParams)) + return null; + + var normalized = NormalizeAdditionalJsonApiParameters(additionalParams); + if (!string.Equals(normalized, additionalParams, StringComparison.Ordinal)) + this.AdditionalJsonApiParameters = normalized; + + var json = $"{{{normalized}}}"; + try + { + if (!this.TryValidateJsonObjectWithDuplicateCheck(json, out var errorMessage)) + return errorMessage; + + return null; + } + catch (JsonException) + { + return T("Invalid JSON."); + } + } + + private static string NormalizeAdditionalJsonApiParameters(string input) + { + var sb = new StringBuilder(input.Length); + var inString = false; + var escape = false; + for (var i = 0; i < input.Length; i++) + { + var c = input[i]; + if (inString) + { + sb.Append(c); + if (escape) + { + escape = false; + continue; + } + + if (c == '\\') + { + escape = true; + continue; + } + + if (c == '"') + inString = false; + + continue; + } + + if (c == '"') + { + inString = true; + sb.Append(c); + continue; + } + + if (TryReadToken(input, i, "True", out var tokenLength)) + { + sb.Append("true"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "False", out tokenLength)) + { + sb.Append("false"); + i += tokenLength - 1; + continue; + } + + if (TryReadToken(input, i, "Null", out tokenLength)) + { + sb.Append("null"); + i += tokenLength - 1; + continue; + } + + sb.Append(c); + } + + return sb.ToString(); + } + + private static bool TryReadToken(string input, int startIndex, string token, out int tokenLength) + { + tokenLength = 0; + if (startIndex + token.Length > input.Length) + return false; + + if (!input.AsSpan(startIndex, token.Length).SequenceEqual(token)) + return false; + + var beforeIndex = startIndex - 1; + if (beforeIndex >= 0 && IsIdentifierChar(input[beforeIndex])) + return false; + + var afterIndex = startIndex + token.Length; + if (afterIndex < input.Length && IsIdentifierChar(input[afterIndex])) + return false; + + tokenLength = token.Length; + return true; + } + + private static bool IsIdentifierChar(char c) => char.IsLetterOrDigit(c) || c == '_'; + + private bool TryValidateJsonObjectWithDuplicateCheck(string json, out string? errorMessage) + { + errorMessage = null; + var bytes = Encoding.UTF8.GetBytes(json); + var reader = new Utf8JsonReader(bytes, new JsonReaderOptions + { + AllowTrailingCommas = false, + CommentHandling = JsonCommentHandling.Disallow + }); + + var objectStack = new Stack>(); + while (reader.Read()) + { + switch (reader.TokenType) + { + case JsonTokenType.StartObject: + objectStack.Push(new HashSet(StringComparer.Ordinal)); + break; + + case JsonTokenType.EndObject: + if (objectStack.Count > 0) + objectStack.Pop(); + break; + + case JsonTokenType.PropertyName: + if (objectStack.Count == 0) + { + errorMessage = T("Additional API parameters must form a JSON object."); + return false; + } + + var name = reader.GetString() ?? string.Empty; + if (!objectStack.Peek().Add(name)) + { + errorMessage = string.Format(T("Duplicate key '{0}' found."), name); + return false; + } + break; + } + } + + if (objectStack.Count != 0) + { + errorMessage = T("Invalid JSON."); + return false; + } + + return true; } private string GetExpertStyles => this.showExpertSettings ? "border-2 border-dashed rounded pa-2" : string.Empty; @@ -345,4 +509,4 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId "top_p": 0.9, "frequency_penalty": 0.0 """; -} \ No newline at end of file +}