Adjusted alert window design and implemented drag & drop security checks

This commit is contained in:
hart_s3 2026-08-07 15:39:56 +02:00
parent ac3f903733
commit f1654889b5
7 changed files with 191 additions and 38 deletions

View File

@ -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"

View File

@ -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;
@ -87,6 +88,9 @@ public partial class AttachDocuments : MSGComponentBase
[Parameter]
public Func<string, Task<ChatThread?>> EnsureOwnerChatAsync { get; set; } = _ => Task.FromResult<ChatThread?>(null);
[Inject]
private PromptInjectionGuardService PromptInjectionGuardService { get; init; } = null!;
[Inject]
private ILogger<AttachDocuments> 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<DocumentCheckDialog>(T("Document Preview"), dialogParameters, DialogOptions.FULLSCREEN);
}
private async Task<bool> 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);
}
}

View File

@ -17,7 +17,7 @@
<MudStack Spacing="0">
<MudText Typo="Typo.h5">
@T("Danger detected")
@T("Security warning")
</MudText>
<MudText Typo="Typo.body2"
@ -26,6 +26,35 @@
</MudText>
</MudStack>
</MudStack>
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mt-4">
<Content>
<MudText Typo="Typo.body2">
@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.")
</MudText>
</Content>
</MudAlert>
</MudPaper>
<MudPaper Class="pa-6 mb-4" Elevation="0" Style="background:#f0fff0;border:1px solid #90ee90;">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="3">
<MudAvatar Size="Size.Large"
Color="Color.Success"
Variant="Variant.Filled">
<MudIcon Icon="@Icons.Material.Filled.Check"/>
</MudAvatar>
<MudStack Spacing="0">
<MudText Typo="Typo.h5">
@T("Security active")
</MudText>
<MudText Typo="Typo.body2"
Color="Color.Success">
@T("AI Studio has reliably detected and blocked the suspicious content. Your applications and data remain protected. Please review the content you uploaded.")
</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudDivider />
@ -41,12 +70,6 @@
</MudStack>
<MudCollapse Expanded="@showPromptInjectionInformation">
<MudText Typo="Typo.body2" Class="mb-4">
<strong>
@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.")
</strong>
</MudText>
<MudPaper Outlined="true"
Class="pa-4 mb-4">
<MudGrid>

View File

@ -367,7 +367,7 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
};
var dialogReference = await this.DialogService.ShowAsync<PromptInjectionAlertDialog>(
T("Prompt Injection Detected"),
T("Security notice"),
dialogParameters,
DialogOptions.BLOCKING_FULLSCREEN);

View File

@ -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."

View File

@ -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<string> 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);

View File

@ -9,7 +9,7 @@ public sealed class PromptInjectionScanner(ILogger<PromptInjectionScanner> 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<PromptInjectionScanner> 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<PromptInjectionScanner> 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<PromptInjectionScanner> logge
private static void AddFinding(List<PromptInjectionFinding> findings, HashSet<string> 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<char> 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()