diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 687ee48c..d759bba3 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -5575,8 +5575,20 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINGGUIDELINEDIALOG::T4250996615"] = "P
-- Chat attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1071345316"] = "Chat attachment"
--- Danger detected
-UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1081126836"] = "Danger detected"
+-- The file '{0}' could not be checked for prompt injection and was not attached.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T2404630365"] = "The file '{0}' could not be checked for prompt injection and was not attached."
+
+-- Security warning
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1862129203"] = "Security warning"
+
+-- AI Studio has reliably detected and blocked the suspicious content. Your applications and data remain protected. Please review the content you uploaded.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T3661810823"] = "AI Studio has reliably detected and blocked the suspicious content. Your applications and data remain protected. Please review the content you uploaded."
+
+-- Security active
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T4212328947"] = "Security active"
+
+-- Security notice
+UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4004397997"] = "Security notice"
-- Content source
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1129278507"] = "Content source"
@@ -7339,9 +7351,6 @@ UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2936083926"] = "AI Studio could
-- Writer
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T2979224202"] = "Writer"
--- Prompt Injection Detected
-UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T3580322580"] = "Prompt Injection Detected"
-
-- Show details
UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T3692372066"] = "Show details"
diff --git a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
index 4b4274fd..6e4a151f 100644
--- a/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
+++ b/app/MindWork AI Studio/Components/AttachDocuments.razor.cs
@@ -5,6 +5,7 @@ using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using AIStudio.Tools.Validation;
+using AIStudio.Tools.Security;
using Microsoft.AspNetCore.Components;
@@ -86,7 +87,10 @@ public partial class AttachDocuments : MSGComponentBase
/// Creates and persists a draft owner after media import confirmation.
[Parameter]
public Func> EnsureOwnerChatAsync { get; set; } = _ => Task.FromResult(null);
-
+
+ [Inject]
+ private PromptInjectionGuardService PromptInjectionGuardService { get; init; } = null!;
+
[Inject]
private ILogger Logger { get; set; } = null!;
@@ -457,10 +461,7 @@ public partial class AttachDocuments : MSGComponentBase
if (!canAddRegularFiles)
break;
- if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, path, this.ValidateMediaFileTypes, this.Provider))
- continue;
-
- this.DocumentPaths.Add(FileAttachment.FromPath(path));
+ await this.TryAddFileAsync(path);
}
if (mediaPaths.Count is 0)
@@ -529,4 +530,34 @@ public partial class AttachDocuments : MSGComponentBase
await this.DialogService.ShowAsync(T("Document Preview"), dialogParameters, DialogOptions.FULLSCREEN);
}
-}
\ No newline at end of file
+
+ private async Task TryAddFileAsync(string filePath)
+ {
+ if (!await FileExtensionValidation.IsExtensionValidWithNotifyAsync(FileExtensionValidation.UseCase.ATTACHING_CONTENT, filePath, this.ValidateMediaFileTypes, this.Provider))
+ return false;
+
+ var attachment = FileAttachment.FromPath(filePath);
+ if (attachment.Type is FileAttachmentType.DOCUMENT && this.PromptInjectionGuardService.IsProtectionEnabled)
+ {
+ try
+ {
+ var fileContent = await this.RustService.ReadArbitraryFileData(filePath, int.MaxValue);
+ await this.PromptInjectionGuardService.EnsureSafeForLlmAsync(fileContent, PromptInjectionSource.ChatAttachment(filePath));
+ }
+ catch (PromptInjectionBlockedException)
+ {
+ return false;
+ }
+ catch (Exception exception)
+ {
+ this.Logger.LogError(exception, "File attachment '{FilePath}' could not be checked for prompt injection and will not be attached.", filePath);
+ await this.MessageBus.SendError(new(
+ Icons.Material.Filled.Cancel,
+ string.Format(T("The file '{0}' could not be checked for prompt injection and was not attached."), attachment.FileName)));
+ return false;
+ }
+ }
+
+ return this.DocumentPaths.Add(attachment);
+ }
+}
diff --git a/app/MindWork AI Studio/Dialogs/PromptInjectionAlertDialog.razor b/app/MindWork AI Studio/Dialogs/PromptInjectionAlertDialog.razor
index f2fbc798..cc777cac 100644
--- a/app/MindWork AI Studio/Dialogs/PromptInjectionAlertDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/PromptInjectionAlertDialog.razor
@@ -17,7 +17,7 @@
- @T("Danger detected")
+ @T("Security warning")
+
+
+
+
+ @T("Typical attacks on AI systems (e.g. prompt injection) hide instructions within untrusted content to trick an AI model into ignoring its intended rules or performing unintended actions.")
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @T("Security active")
+
+
+
+ @T("AI Studio has reliably detected and blocked the suspicious content. Your applications and data remain protected. Please review the content you uploaded.")
+
+
+
@@ -41,12 +70,6 @@
-
-
- @T("Typical attacks on AI systems (e.g. prompt injection) hide instructions within untrusted content to trick an AI model into ignoring its intended rules or performing unintended actions.")
-
-
-
diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
index aa58004b..7faa623a 100644
--- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs
@@ -367,7 +367,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
};
var dialogReference = await this.DialogService.ShowAsync(
- T("Prompt Injection Detected"),
+ T("Security notice"),
dialogParameters,
DialogOptions.BLOCKING_FULLSCREEN);
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index 0361d1ee..8b38751e 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -4278,15 +4278,28 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::WORKSPACES::T1016188706"] = "Möchten Sie
-- Chat attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1071345316"] = "Dateianhang"
--- Danger detected
-UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1081126836"] = "Gefahr erkannt"
+-- The file '{0}' could not be checked for prompt injection and was not attached.
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ATTACHDOCUMENTS::T2404630365"] = "Die Datei '{0}' konnte nicht auf Prompt-Injection überprüft werden und wurde nicht angehängt."
+
+-- Security warning
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1862129203"] = "Sicherheitswarnung"
+
+
+-- AI Studio has reliably detected and blocked the suspicious content. Your applications and data remain protected. Please review the content you uploaded.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T3661810823"] = "AI Studio hat den verdächtigen Inhalt zuverlässig erkannt und blockiert. Ihre Anwendungen und Daten bleiben geschützt. Bitte überprüfen Sie den von Ihnen hochgeladenen Inhalt."
+
+-- Security active
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T4212328947"] = "Sicherheit aktiv"
+
+-- Typical attacks on AI systems
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T4221674400"] = "Typische Angriffe auf KI-Systeme (z.B. Prompt Injection) verbergen Anweisungen in nicht vertrauenswürdigen Inhalten, um ein KI-Modell dazu zu bringen, seine vorgesehenen Regeln zu ignorieren oder ungewollte Aktionen auszuführen."
+
+-- Security notice
+UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T4004397997"] = "Sicherheitshinweis"
-- Content source
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T1129278507"] = "Datei"
--- Prompt injection hides instructions in untrusted content to make an AI model ignore its intended rules or perform unintended actions.
-UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T4221674400"] = "Typische Angriffe auf KI-Systeme (z.B. Prompt Injection) verbergen Anweisungen in nicht vertrauenswürdigen Inhalten, um ein KI-Modell dazu zu bringen, seine vorgesehenen Regeln zu ignorieren oder ungewollte Aktionen auszuführen."
-
-- Attempt to override instructions
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROMPTINJECTIONALERTDIALOG::T161976090"] = "Versuch, Anweisungen zu überschreiben"
@@ -4371,8 +4384,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T4245463281"]
-- Checks web content, file attachments, retrieval context, and similar external input for prompt-injection patterns before it is sent to a model or agent.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T69192586"] = "Prüft Webinhalte, Dateianhänge, Abrufkontext und ähnliche externe Eingaben auf Prompt-Injection-Muster, bevor sie an ein Modell oder einen Agenten gesendet werden."
--- Prompt Injection Detected
-UI_TEXT_CONTENT["AISTUDIO::LAYOUT::MAINLAYOUT::T3580322580"] = "Prompt-Injection erkannt"
-- AI Studio blocked content from '{0}' because it looks like a prompt-injection attempt.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SECURITY::PROMPTINJECTIONGUARDSERVICE::T3560909296"] = "AI Studio hat Inhalte von '{0}' blockiert, da es sich um einen möglichen Prompt-Injection-Versuch handelt."
diff --git a/app/MindWork AI Studio/Tools/Security/PromptInjectionGuardService.cs b/app/MindWork AI Studio/Tools/Security/PromptInjectionGuardService.cs
index 28d1d6b8..017578f9 100644
--- a/app/MindWork AI Studio/Tools/Security/PromptInjectionGuardService.cs
+++ b/app/MindWork AI Studio/Tools/Security/PromptInjectionGuardService.cs
@@ -12,9 +12,11 @@ public sealed class PromptInjectionGuardService(
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PromptInjectionGuardService).Namespace, nameof(PromptInjectionGuardService));
+ public bool IsProtectionEnabled => settingsManager.ConfigurationData.Chat.EnablePromptInjectionProtection;
+
public async Task EnsureSafeForLlmAsync(string text, PromptInjectionSource source)
{
- if (!settingsManager.ConfigurationData.Chat.EnablePromptInjectionProtection || string.IsNullOrWhiteSpace(text))
+ if (!this.IsProtectionEnabled || string.IsNullOrWhiteSpace(text))
return text;
var result = scanner.Scan(text, source);
diff --git a/app/MindWork AI Studio/Tools/Security/PromptInjectionScanner.cs b/app/MindWork AI Studio/Tools/Security/PromptInjectionScanner.cs
index 99e0c419..665572d7 100644
--- a/app/MindWork AI Studio/Tools/Security/PromptInjectionScanner.cs
+++ b/app/MindWork AI Studio/Tools/Security/PromptInjectionScanner.cs
@@ -9,7 +9,7 @@ public sealed class PromptInjectionScanner(ILogger logge
private const int MAX_DECODED_CANDIDATES_PER_ENCODING = 12;
private const int MAX_DECODED_TEXT_LENGTH = 12_000;
private const int MAX_FINDINGS = 8;
- private const int SNIPPET_RADIUS = 80;
+ private const int MAX_SNIPPET_LENGTH = 240;
private static readonly IReadOnlyDictionary<(int Length, char First, char Last), string[]> TYPOGLYCEMIA_KEYWORDS =
CreateTypoglycemiaKeywordIndex();
@@ -70,8 +70,8 @@ public sealed class PromptInjectionScanner(ILogger logge
if (!match.Success)
continue;
- var snippet = ExtractSnippet(text, match.Index, match.Length, SNIPPET_RADIUS);
- AddFinding(findings, findingKeys, new(rule.Id, rule.Category, stage, snippet));
+ var snippet = ExtractSnippet(text, match.Index, match.Length);
+ AddFinding(findings, findingKeys, new(rule.Id, rule.Category, snippet));
}
}
@@ -131,8 +131,8 @@ public sealed class PromptInjectionScanner(ILogger logge
if (!IsTypoglycemiaVariant(token, keyword))
continue;
- var snippet = ExtractSnippet(text, match.Index, match.Length, SNIPPET_RADIUS);
- AddFinding(findings, findingKeys, new($"typoglycemia:{keyword}", "evasion", "typoglycemia", snippet));
+ var snippet = ExtractSnippet(text, match.Index, match.Length);
+ AddFinding(findings, findingKeys, new($"typoglycemia:{keyword}", "evasion", snippet));
break;
}
}
@@ -265,17 +265,94 @@ public sealed class PromptInjectionScanner(ILogger logge
private static void AddFinding(List findings, HashSet findingKeys, PromptInjectionFinding finding)
{
- var key = $"{finding.RuleId}|{finding.DetectionStage}|{finding.Snippet}";
+ var key = $"{finding.Category}|{finding.Snippet}";
if (findingKeys.Add(key))
findings.Add(finding);
}
- private static string ExtractSnippet(string text, int index, int length, int radius)
+ private static string ExtractSnippet(string text, int index, int length)
{
- var start = Math.Max(0, index - radius);
- var end = Math.Min(text.Length, index + length + radius);
- var snippet = text[start..end].Replace('\r', ' ').Replace('\n', ' ').Trim();
- return snippet.Length > radius * 2 ? $"{snippet[..(radius * 2)]}..." : snippet;
+ var matchStart = Math.Clamp(index, 0, text.Length);
+ var matchEnd = Math.Clamp(index + length, matchStart, text.Length);
+ var sentenceStart = FindSentenceStart(text, matchStart);
+ var sentenceEnd = FindSentenceEnd(text, matchEnd);
+
+ while (sentenceStart < matchStart && char.IsWhiteSpace(text[sentenceStart]))
+ sentenceStart++;
+
+ while (sentenceEnd > matchEnd && char.IsWhiteSpace(text[sentenceEnd - 1]))
+ sentenceEnd--;
+
+ if (sentenceEnd - sentenceStart <= MAX_SNIPPET_LENGTH)
+ return NormalizeSnippet(text[sentenceStart..sentenceEnd]);
+
+ var matchLength = matchEnd - matchStart;
+ if (matchLength >= MAX_SNIPPET_LENGTH - 6)
+ return NormalizeSnippet(text[matchStart..matchEnd]);
+
+ var contextBudget = MAX_SNIPPET_LENGTH - 6 - matchLength;
+ var leftAvailable = matchStart - sentenceStart;
+ var rightAvailable = sentenceEnd - matchEnd;
+ var leftLength = Math.Min(leftAvailable, contextBudget / 2);
+ var rightLength = Math.Min(rightAvailable, contextBudget - leftLength);
+ var remainingBudget = contextBudget - leftLength - rightLength;
+
+ leftLength += Math.Min(leftAvailable - leftLength, remainingBudget);
+ remainingBudget = contextBudget - leftLength - rightLength;
+ rightLength += Math.Min(rightAvailable - rightLength, remainingBudget);
+
+ var snippetStart = matchStart - leftLength;
+ var snippetEnd = matchEnd + rightLength;
+ var snippet = NormalizeSnippet(text[snippetStart..snippetEnd]);
+ var prefix = snippetStart > sentenceStart ? "..." : string.Empty;
+ var suffix = snippetEnd < sentenceEnd ? "..." : string.Empty;
+ return $"{prefix}{snippet}{suffix}";
+ }
+
+ private static int FindSentenceStart(string text, int matchStart)
+ {
+ for (var index = matchStart - 1; index >= 0; index--)
+ {
+ if (IsSentenceBoundary(text[index]))
+ return index + 1;
+ }
+
+ return matchStart;
+ }
+
+ private static int FindSentenceEnd(string text, int matchEnd)
+ {
+ for (var index = matchEnd; index < text.Length; index++)
+ {
+ if (IsSentenceBoundary(text[index]))
+ return index + 1;
+ }
+
+ return matchEnd;
+ }
+
+ private static bool IsSentenceBoundary(char character) => character is '.' or '!' or '?' or '\r' or '\n';
+
+ private static string NormalizeSnippet(ReadOnlySpan snippet)
+ {
+ var normalized = new StringBuilder(snippet.Length);
+ var previousCharacterWasWhitespace = false;
+ foreach (var character in snippet)
+ {
+ if (char.IsWhiteSpace(character))
+ {
+ if (normalized.Length > 0 && !previousCharacterWasWhitespace)
+ normalized.Append(' ');
+
+ previousCharacterWasWhitespace = true;
+ continue;
+ }
+
+ normalized.Append(character);
+ previousCharacterWasWhitespace = false;
+ }
+
+ return normalized.ToString().Trim();
}
private static IReadOnlyDictionary<(int Length, char First, char Last), string[]> CreateTypoglycemiaKeywordIndex()