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
Original file line number Diff line number Diff line change
@@ -1,68 +1,40 @@
namespace CrestApps.Core.AI.Security;

/// <summary>
/// Per-profile settings that control prompt security behavior.
/// Per-profile anti-spam throttling overrides for an AI Profile.
/// Stored on <see cref="Models.AIProfile.Settings"/> using <c>profile.WithSettings(new PromptSecurityProfileSettings { ... })</c>.
/// When present, these settings override the site-level <see cref="PromptSecurityOptions"/> defaults
/// for the specific profile.
/// When present, these values override the site-level throttling defaults defined on
/// <see cref="PromptSecurityOptions"/> for the specific profile, letting each use case raise or lower
/// its limits. High-level input and output security guards (injection detection, output filtering,
/// security preamble, input delimiters, blocking threshold, and maximum prompt length) are intentionally
/// not part of this model; those remain global concerns configured through <see cref="PromptSecurityOptions"/>.
/// </summary>
public sealed class PromptSecurityProfileSettings
{
/// <summary>
/// Gets or sets a value indicating whether the prompt security layer is enabled for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// Set to <see langword="false"/> to explicitly disable security for this profile.
/// </summary>
public bool? IsEnabled { get; set; }

/// <summary>
/// Gets or sets a value indicating whether injection pattern detection is enabled for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public bool? EnableInjectionDetection { get; set; }

/// <summary>
/// Gets or sets a value indicating whether output security filtering is enabled for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public bool? EnableOutputFiltering { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the hardened security preamble is prepended
/// to the system prompt for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public bool? EnableSecurityPreamble { get; set; }

/// <summary>
/// Gets or sets a value indicating whether user messages are wrapped with boundary
/// delimiters for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public bool? EnableInputDelimiters { get; set; }

/// <summary>
/// Gets or sets the maximum allowed prompt length for this profile.
/// Gets or sets the maximum number of messages allowed within the rate limit window for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// Set to <c>0</c> to explicitly disable message rate limiting for this profile.
/// </summary>
public int? MaxPromptLength { get; set; }
public int? MaxMessagesPerWindow { get; set; }

/// <summary>
/// Gets or sets the minimum risk level at which prompts are blocked for this profile.
/// Gets or sets the rate limit window duration for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public PromptRiskLevel? BlockingThreshold { get; set; }
public TimeSpan? RateLimitWindow { get; set; }

/// <summary>
/// Gets or sets the maximum number of messages allowed within the rate limit window for this profile.
/// Gets or sets the maximum number of anonymous chat sessions that can be started
/// within the anonymous session rate-limit window for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// Set to <c>0</c> to explicitly disable rate limiting for this profile.
/// Set to <c>0</c> to explicitly disable anonymous session-start throttling for this profile.
/// </summary>
public int? MaxMessagesPerWindow { get; set; }
public int? MaxAnonymousSessionsPerWindow { get; set; }

/// <summary>
/// Gets or sets the rate limit window duration for this profile.
/// Gets or sets the anonymous session-start rate-limit window duration for this profile.
/// When <see langword="null"/>, the site-level default is used.
/// </summary>
public TimeSpan? RateLimitWindow { get; set; }
public TimeSpan? AnonymousSessionRateLimitWindow { get; set; }
}
1 change: 1 addition & 0 deletions src/CrestApps.Core.Docs/docs/changelog/v1.0.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description: Initial standalone release notes for the CrestApps.Core repository.

- ships the shared abstractions, infrastructure, AI runtime, provider integrations, and protocol packages under the `CrestApps.Core` name
- assigns stable first-party anonymous visitor IDs to AI chat sessions, uses those visitor IDs for unique-visitor analytics, makes remote-address capture configurable with privacy-first hashed defaults plus optional plain-text or encrypted-at-rest storage, protects anonymous chat session starts with shared ASP.NET Core plus hub-level rate limiting, and documents how hosts can tune thresholds or replace the default endpoint policy with their own `Microsoft.AspNetCore.RateLimiting` policy
- lets AI Profiles and AI Profile-source templates override the site-wide anti-spam throttle limits through `PromptSecurityProfileSettings` (max messages per window, message window, max anonymous sessions per window, and anonymous session window), with each unset value falling back to the `PromptSecurityOptions` site default so profiles can raise or lower quotas per use case; both the message and anonymous session-start limiters honor the overrides, while high-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only concerns
- defers AI Profile initial-prompt persistence until the first real user prompt arrives and stops the sample chat widgets from auto-creating empty sessions on initial page load
- includes a reference MVC host and an Aspire host for local composition and testing
- includes a dedicated `CrestApps.Core.Tests` project for framework validation
Expand Down
14 changes: 8 additions & 6 deletions src/CrestApps.Core.Docs/docs/core/ai-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,17 @@ Profiles can opt into user memory so experiences can carry durable context forwa

That toggle is stored directly as `MemoryMetadata`, so profile and template consumers read and write one shared metadata shape instead of carrying legacy memory-setting aliases forward.

### 8. Prompt security
### 8. Anti-spam throttling

Profiles can also override the site-level prompt security defaults through `PromptSecurityProfileSettings`.
Profiles can override the site-level anti-spam throttle limits through `PromptSecurityProfileSettings`.

That lets you keep a strong global baseline while adjusting individual profile behavior for cases such as:
That lets you keep a strong global baseline while raising or lowering throttle quotas for individual profiles, for cases such as:

- stricter blocking for high-sensitivity assistants
- longer prompt limits for carefully managed internal workflows
- disabling the security layer for intentionally operator-controlled profiles
- tighter per-minute message limits for public, unauthenticated widgets
- higher limits for carefully managed internal or authenticated workflows
- adjusting anonymous session-start quotas for a specific use case

Only anti-spam throttling is per-profile. High-level input and output security guards (injection detection, output filtering, security preamble, input delimiters, blocking threshold, and maximum prompt length) remain global-only and are configured through `PromptSecurityOptions`.

See [Prompt Security](./prompt-security.md) for the full option set, scoring model, and limitations.

Expand Down
28 changes: 14 additions & 14 deletions src/CrestApps.Core.Docs/docs/core/prompt-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,22 +149,20 @@ The prompt security layer is controlled by `PromptSecurityOptions`.
The framework supports two layers of configuration:

1. **site-level defaults** through `PromptSecurityOptions`
2. **per-profile overrides** through `PromptSecurityProfileSettings`
2. **per-profile anti-spam throttle overrides** through `PromptSecurityProfileSettings`

Both MVC and Blazor sample hosts expose the site defaults in admin settings, and AI Profiles plus AI Profile-source templates can override the default behavior.
Both MVC and Blazor sample hosts expose the site defaults in admin settings. AI Profiles and AI Profile-source templates can override the anti-spam throttle limits so each use case can raise or lower its quotas.

Per-profile overrides support:
Per-profile overrides are intentionally scoped to anti-spam throttling only:

- enabling or disabling the prompt security layer
- enabling or disabling injection detection
- enabling or disabling output filtering
- enabling or disabling the security preamble
- enabling or disabling input delimiters
- overriding maximum prompt length
- overriding the blocking threshold
- overriding rate limit settings (messages per window, window duration)
- `MaxMessagesPerWindow` — maximum messages allowed within the message window
- `RateLimitWindow` — the message sliding-window duration
- `MaxAnonymousSessionsPerWindow` — maximum anonymous session starts within the session window
- `AnonymousSessionRateLimitWindow` — the anonymous session-start window duration

That model lets you keep strong defaults globally while allowing carefully chosen profiles to be more permissive or more strict.
High-level input and output security guards — injection detection, output filtering, the security preamble, input delimiters, the blocking threshold, and the maximum prompt length — remain **global concerns** configured only through `PromptSecurityOptions`. They are deliberately not overridable per profile so protective posture stays consistent across every profile.

That model lets you keep strong global guards while allowing carefully chosen profiles to be more permissive or more strict on throttling.

## Rate limiting

Expand Down Expand Up @@ -196,17 +194,19 @@ The default limiter behavior is privacy-first but configurable through `AIChatRa

### Per-profile override

Rate limiting can be customized per AI Profile using `PromptSecurityProfileSettings`:
Anti-spam throttling can be customized per AI Profile (or AI Profile-source template) using `PromptSecurityProfileSettings`. Any field left `null` falls back to the site-level default:

```csharp
profile.WithSettings(new PromptSecurityProfileSettings
{
MaxMessagesPerWindow = 10,
RateLimitWindow = TimeSpan.FromMinutes(2),
MaxAnonymousSessionsPerWindow = 3,
AnonymousSessionRateLimitWindow = TimeSpan.FromMinutes(10),
});
```

Set `MaxMessagesPerWindow` to `0` on a profile to disable rate limiting for that profile even when site-level rate limiting is enabled.
Both the message throttle (`DefaultChatRateLimiter`) and the anonymous session-start throttle (`DefaultChatSessionStartRateLimiter`) honor these overrides, falling back to the site defaults on `PromptSecurityOptions` when a value is not set. Set `MaxMessagesPerWindow` (or `MaxAnonymousSessionsPerWindow`) to `0` on a profile to disable that throttle for the profile even when site-level rate limiting is enabled.

### How it works

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using CrestApps.Core.AI.Models;
using CrestApps.Core.Support;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -7,7 +8,8 @@ namespace CrestApps.Core.AI.Security;

/// <summary>
/// Default implementation of <see cref="IChatSessionStartRateLimiter"/> that limits
/// anonymous session creation using a sliding window.
/// anonymous session creation using a sliding window. Per-profile overrides on
/// <see cref="PromptSecurityProfileSettings"/> take precedence over the site-level defaults.
/// </summary>
public sealed class DefaultChatSessionStartRateLimiter : IChatSessionStartRateLimiter
{
Expand Down Expand Up @@ -50,7 +52,11 @@ public ValueTask<RateLimitResult> EvaluateAsync(PromptSecurityContext context, C
}

var options = _options.Value;
var maxSessions = options.MaxAnonymousSessionsPerWindow;

// Resolve per-profile anti-spam overrides, falling back to site-level defaults.
var profileSettings = context.Profile?.TryGetSettings<PromptSecurityProfileSettings>(out var ps) == true ? ps : null;
var maxSessions = profileSettings?.MaxAnonymousSessionsPerWindow ?? options.MaxAnonymousSessionsPerWindow;
var window = profileSettings?.AnonymousSessionRateLimitWindow ?? options.AnonymousSessionRateLimitWindow;

if (maxSessions <= 0)
{
Expand All @@ -64,7 +70,6 @@ public ValueTask<RateLimitResult> EvaluateAsync(PromptSecurityContext context, C
return ValueTask.FromResult(RateLimitResult.Allowed);
}

var window = options.AnonymousSessionRateLimitWindow;
var now = _timeProvider.GetUtcNow();
var windowStart = now - window;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace CrestApps.Core.AI.Security;
/// <summary>
/// Default implementation of <see cref="IPromptSecurityService"/> that validates user prompts
/// against known prompt injection patterns and configurable security policies.
/// Respects per-profile security settings that override site-level defaults.
/// Security guards are governed by the site-level <see cref="PromptSecurityOptions"/>, while the
/// injected rate limiter honors per-profile anti-spam throttle overrides.
/// </summary>
public sealed class DefaultPromptSecurityService : IPromptSecurityService
{
Expand Down Expand Up @@ -50,16 +51,8 @@ public async Task<PromptSecurityResult> ValidateInputAsync(PromptSecurityContext

var siteOptions = _options.Value;

// Resolve per-profile security settings.
var profileSettings = context.Profile?.TryGetSettings<PromptSecurityProfileSettings>(out var ps) == true ? ps : null;

// Check if security is entirely disabled for this profile.
if (profileSettings?.IsEnabled == false)
{
return PromptSecurityResult.Safe;
}

// Rate limit check (before expensive regex evaluation).
// The rate limiter honors per-profile anti-spam throttle overrides.
var rateLimitResult = await _rateLimiter.EvaluateAsync(context, cancellationToken);

if (rateLimitResult.IsThrottled)
Expand All @@ -77,7 +70,7 @@ public async Task<PromptSecurityResult> ValidateInputAsync(PromptSecurityContext
return rateLimitBlockedResult;
}

var injectionDetectionEnabled = profileSettings?.EnableInjectionDetection ?? siteOptions.EnableInjectionDetection;
var injectionDetectionEnabled = siteOptions.EnableInjectionDetection;

if (!injectionDetectionEnabled)
{
Expand All @@ -89,8 +82,8 @@ public async Task<PromptSecurityResult> ValidateInputAsync(PromptSecurityContext
return PromptSecurityResult.Safe;
}

var blockingThreshold = profileSettings?.BlockingThreshold ?? siteOptions.BlockingThreshold;
var maxPromptLength = profileSettings?.MaxPromptLength ?? siteOptions.MaxPromptLength;
var blockingThreshold = siteOptions.BlockingThreshold;
var maxPromptLength = siteOptions.MaxPromptLength;
var evaluationContext = new PromptSecurityEvaluationContext
{
OriginalInput = context.Prompt,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,16 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
// Only apply security to AI Profile-based chats.
// Chat Interactions give the user full control (system prompt, model, MCP)
// so the security layer is not applicable.
if (context.Resource is not AIProfile profile)
if (context.Resource is not AIProfile)
{
return;
}

// Resolve effective options by merging site-level defaults with per-profile overrides.
var effectiveOptions = ResolveEffectiveOptions(profile);

// If security is entirely disabled for this profile, skip.
if (!effectiveOptions.IsEnabled)
{
return;
}
// Security guards are governed globally by the site-level options.
var siteOptions = _options.Value;

// Prepend the security preamble to the system message.
if (effectiveOptions.EnableSecurityPreamble)
if (siteOptions.EnableSecurityPreamble)
{
var preamble = await _templateService.RenderAsync(SecurityPreambleTemplateId, cancellationToken: cancellationToken);

Expand Down Expand Up @@ -113,7 +107,7 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
}

// Wrap user message with delimiters to establish clear input boundaries.
if (effectiveOptions.EnableInputDelimiters && !string.IsNullOrEmpty(context.OrchestrationContext.UserMessage))
if (siteOptions.EnableInputDelimiters && !string.IsNullOrEmpty(context.OrchestrationContext.UserMessage))
{
// Sanitize user input by removing any injected delimiter tokens.
var sanitizedMessage = context.OrchestrationContext.UserMessage
Expand All @@ -140,38 +134,4 @@ public async Task BuiltAsync(OrchestrationContextBuiltContext context, Cancellat
}
}
}

private EffectiveSecurityOptions ResolveEffectiveOptions(AIProfile profile)
{
var siteOptions = _options.Value;
var profileSettings = profile.TryGetSettings<PromptSecurityProfileSettings>(out var settings) ? settings : null;

return new EffectiveSecurityOptions
{
IsEnabled = profileSettings?.IsEnabled ?? true,
EnableSecurityPreamble = profileSettings?.EnableSecurityPreamble ?? siteOptions.EnableSecurityPreamble,
EnableInputDelimiters = profileSettings?.EnableInputDelimiters ?? siteOptions.EnableInputDelimiters,
EnableInjectionDetection = profileSettings?.EnableInjectionDetection ?? siteOptions.EnableInjectionDetection,
EnableOutputFiltering = profileSettings?.EnableOutputFiltering ?? siteOptions.EnableOutputFiltering,
MaxPromptLength = profileSettings?.MaxPromptLength ?? siteOptions.MaxPromptLength,
BlockingThreshold = profileSettings?.BlockingThreshold ?? siteOptions.BlockingThreshold,
};
}

private sealed class EffectiveSecurityOptions
{
public bool IsEnabled { get; init; }

public bool EnableSecurityPreamble { get; init; }

public bool EnableInputDelimiters { get; init; }

public bool EnableInjectionDetection { get; init; }

public bool EnableOutputFiltering { get; init; }

public int MaxPromptLength { get; init; }

public PromptRiskLevel BlockingThreshold { get; init; }
}
}
Loading
Loading