Desktop alerts when suspicious unicode characters found in Recipe (#4080)
This commit is contained in:
@@ -389,7 +389,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
|
||||
super::routes::schedule::sessions_handler,
|
||||
super::routes::recipe::create_recipe,
|
||||
super::routes::recipe::encode_recipe,
|
||||
super::routes::recipe::decode_recipe
|
||||
super::routes::recipe::decode_recipe,
|
||||
super::routes::recipe::scan_recipe
|
||||
),
|
||||
components(schemas(
|
||||
super::routes::config_management::UpsertConfigQuery,
|
||||
@@ -456,6 +457,8 @@ impl<'__s> ToSchema<'__s> for AnnotatedSchema {
|
||||
super::routes::recipe::EncodeRecipeResponse,
|
||||
super::routes::recipe::DecodeRecipeRequest,
|
||||
super::routes::recipe::DecodeRecipeResponse,
|
||||
super::routes::recipe::ScanRecipeRequest,
|
||||
super::routes::recipe::ScanRecipeResponse,
|
||||
goose::recipe::Recipe,
|
||||
goose::recipe::Author,
|
||||
goose::recipe::Settings,
|
||||
|
||||
@@ -56,6 +56,16 @@ pub struct DecodeRecipeResponse {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, ToSchema)]
|
||||
pub struct ScanRecipeRequest {
|
||||
recipe: Recipe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct ScanRecipeResponse {
|
||||
has_security_warnings: bool,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/create",
|
||||
@@ -164,11 +174,31 @@ async fn decode_recipe(
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/recipes/scan",
|
||||
request_body = ScanRecipeRequest,
|
||||
responses(
|
||||
(status = 200, description = "Recipe scanned successfully", body = ScanRecipeResponse),
|
||||
),
|
||||
tag = "Recipe Management"
|
||||
)]
|
||||
async fn scan_recipe(
|
||||
Json(request): Json<ScanRecipeRequest>,
|
||||
) -> Result<Json<ScanRecipeResponse>, StatusCode> {
|
||||
let has_security_warnings = request.recipe.check_for_security_warnings();
|
||||
|
||||
Ok(Json(ScanRecipeResponse {
|
||||
has_security_warnings,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn routes(state: Arc<AppState>) -> Router {
|
||||
Router::new()
|
||||
.route("/recipes/create", post(create_recipe))
|
||||
.route("/recipes/encode", post(encode_recipe))
|
||||
.route("/recipes/decode", post(decode_recipe))
|
||||
.route("/recipes/scan", post(scan_recipe))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
|
||||
use crate::agents::extension::ExtensionConfig;
|
||||
use crate::agents::types::RetryConfig;
|
||||
use crate::utils::contains_unicode_tags;
|
||||
use serde::de::Deserializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
@@ -253,6 +254,25 @@ pub struct RecipeBuilder {
|
||||
}
|
||||
|
||||
impl Recipe {
|
||||
/// Returns true if harmful content is detected in instructions, prompt, or activities fields
|
||||
pub fn check_for_security_warnings(&self) -> bool {
|
||||
if [self.instructions.as_deref(), self.prompt.as_deref()]
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|&field| contains_unicode_tags(field))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(activities) = &self.activities {
|
||||
return activities
|
||||
.iter()
|
||||
.any(|activity| contains_unicode_tags(activity));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Creates a new RecipeBuilder to construct a Recipe instance
|
||||
///
|
||||
/// # Example
|
||||
@@ -746,4 +766,41 @@ isGlobal: true"#;
|
||||
let extensions = recipe.extensions.unwrap();
|
||||
assert_eq!(extensions.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_for_security_warnings() {
|
||||
let mut recipe = Recipe {
|
||||
version: "1.0.0".to_string(),
|
||||
title: "Test".to_string(),
|
||||
description: "Test".to_string(),
|
||||
instructions: Some("clean instructions".to_string()),
|
||||
prompt: Some("clean prompt".to_string()),
|
||||
extensions: None,
|
||||
context: None,
|
||||
settings: None,
|
||||
activities: Some(vec!["clean activity 1".to_string()]),
|
||||
author: None,
|
||||
parameters: None,
|
||||
response: None,
|
||||
sub_recipes: None,
|
||||
retry: None,
|
||||
};
|
||||
|
||||
assert!(!recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious activities
|
||||
recipe.activities = Some(vec![
|
||||
"clean activity".to_string(),
|
||||
format!("malicious{}activity", '\u{E0041}'),
|
||||
]);
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious instructions
|
||||
recipe.instructions = Some(format!("instructions{}", '\u{E0041}'));
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
|
||||
// Malicious prompt
|
||||
recipe.prompt = Some(format!("prompt{}", '\u{E0042}'));
|
||||
assert!(recipe.check_for_security_warnings());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
/// Check if a character is in the Unicode Tags Block range (U+E0000-U+E007F)
|
||||
/// These characters are invisible and can be used for steganographic attacks
|
||||
fn is_in_unicode_tag_range(c: char) -> bool {
|
||||
matches!(c, '\u{E0000}'..='\u{E007F}')
|
||||
}
|
||||
|
||||
pub fn contains_unicode_tags(text: &str) -> bool {
|
||||
text.chars().any(is_in_unicode_tag_range)
|
||||
}
|
||||
|
||||
/// Sanitize Unicode Tags Block characters from text
|
||||
/// Used to prevent Unicode-based prompt injection attacks
|
||||
///
|
||||
/// This function removes invisible Unicode Tags Block characters (U+E0000-U+E007F)
|
||||
/// that can be used for steganographic attacks while preserving legitimate Unicode.
|
||||
pub fn sanitize_unicode_tags(text: &str) -> String {
|
||||
let normalized: String = text.nfc().collect();
|
||||
|
||||
normalized
|
||||
.chars()
|
||||
.filter(|&c| !matches!(c, '\u{E0000}'..='\u{E007F}'))
|
||||
.filter(|&c| !is_in_unicode_tag_range(c))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -45,6 +51,17 @@ pub fn is_token_cancelled(cancellation_token: &Option<CancellationToken>) -> boo
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_contains_unicode_tags() {
|
||||
// Test detection of Unicode Tags Block characters
|
||||
assert!(contains_unicode_tags("Hello\u{E0041}world"));
|
||||
assert!(contains_unicode_tags("\u{E0000}"));
|
||||
assert!(contains_unicode_tags("\u{E007F}"));
|
||||
assert!(!contains_unicode_tags("Hello world"));
|
||||
assert!(!contains_unicode_tags("Hello 世界 🌍"));
|
||||
assert!(!contains_unicode_tags(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_unicode_tags() {
|
||||
// Test that Unicode Tags Block characters are removed
|
||||
|
||||
Reference in New Issue
Block a user