Llm Data Leakage in Actix
How Llm Data Leakage Manifests in Actix
Llm data leakage in Actix applications typically occurs through improper handling of AI/ML endpoints that inadvertently expose sensitive information. In Actix, this often manifests when LLM endpoints return system prompts, training data, or internal model configurations to unauthorized users.
A common vulnerability pattern in Actix involves LLM endpoints that lack proper authentication checks. Consider this problematic Actix route:
async fn chat_model(
req: HttpRequest,
payload: actix_web::web::Payload,
) -> impl Responder {
let body = actix_web::web::Bytes::from(req.clone().into_body()).await?;
let prompt = String::from_utf8(body.to_vec())?;
// Direct model call without auth
let response = llm_model.generate(prompt).await;
HttpResponse::Ok().json({
"response": response,
"system_prompt": llm_model.get_system_prompt(), // EXPOSURE!
"model_version": llm_model.get_version(), // EXPOSURE!
})
}
This Actix handler leaks critical information through the response: the system prompt reveals the AI's instructions and constraints, the model version exposes internal architecture details, and if the model contains training data about specific organizations or individuals, that data could be returned directly in responses.
Another Actix-specific manifestation involves improper error handling in async LLM operations. When Actix handlers panic or return errors from async LLM calls, stack traces or debug information might be exposed:
async fn chat_model(req: HttpRequest) -> impl Responder {
let prompt = extract_prompt(req).await?;
// No error boundary - panics expose internals
let response = llm_model.generate(prompt).await.unwrap();
HttpResponse::Ok().json({
"response": response,
"debug_info": format!("Processed in {}ms", elapsed_time), // Debug data leakage
})
}
Actix's streaming response capabilities can also introduce leakage when streaming LLM responses without proper sanitization. A vulnerable implementation might stream raw model outputs that include system prompts or training data:
async fn chat_stream(
req: HttpRequest,
payload: actix_web::web::Payload,
) -> impl Responder {
let prompt = extract_prompt(req).await?;
let mut response = HttpResponse::Ok()
.content_type("text/plain")
.streaming({
async_stream::stream! {
// Streaming raw model output without filtering
for chunk in llm_model.generate_stream(prompt).await {
yield chunk;
}
}
});
response
}
The streaming approach makes it harder to sanitize outputs before they reach the client, potentially exposing sensitive training data or system instructions in real-time.
Actix-Specific Detection
Detecting LLM data leakage in Actix applications requires examining both the routing configuration and the handler implementations. Start by scanning your Actix application for LLM-related endpoints using route inspection:
use actix_web::guard;
// Identify LLM endpoints by path patterns
let routes = app_state.routes.clone();
for route in routes {
if route.path.contains("chat") || route.path.contains("llm") ||
route.path.contains("ai") || route.path.contains("model") {
println!("Potential LLM endpoint: {}", route.path);
}
}
For runtime detection, middleBrick's LLM security scanning specifically targets Actix applications by sending crafted prompts to identify data leakage. The scanner tests for system prompt exposure using 27 regex patterns covering formats like ChatML, Llama 2, and Mistral:
#[cfg(test)]
mod llm_security_tests {
use super::*;
use actix_web::{test, http::StatusCode};
#[actix_rt::test]
async fn test_llm_data_leakage() {
let mut app = test::init_service(
App::new().service(chat_model)
).await;
// Test for system prompt leakage
let req = test::TestRequest::post()
.uri("/chat")
.set_payload("SYSTEM")
.to_request();
let resp = test::call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = test::read_body(resp).await;
let body_str = String::from_utf8(body.to_vec()).unwrap();
// Check for system prompt patterns
assert!(!body_str.contains("SYSTEM"));
assert!(!body_str.contains("Assistant will")); // Common system prompt prefix
}
}
middleBrick's active scanning goes beyond simple pattern matching by testing for prompt injection vulnerabilities that could extract system prompts:
// middleBrick would test with sequences like:
let injection_prompts = [
"SYSTEM", // Extract system prompt
"Ignore previous instructions", // Override behavior
"DAN", // Jailbreak attempt
"Translate this to JSON:", // Data exfiltration
];
For Actix applications using streaming responses, detection requires monitoring the actual data flow. You can implement a middleware to detect sensitive data patterns in LLM responses:
use actix_web::{dev::Service, dev::ServiceRequest, dev::ServiceResponse, Error};
pub struct LlmLeakageDetector;
impl Transform for LlmLeakageDetector
where
S: Service,
{
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type InitError = ();
type Transform = LlmLeakageDetectorMiddleware;
fn new_transform(&self, service: S) -> Result {
Ok(LlmLeakageDetectorMiddleware { service })
}
}
pub struct LlmLeakageDetectorMiddleware {
service: S,
}
#[async_trait::async_trait]
impl Transform for LlmLeakageDetector
where
S: Service,
{
async fn on_request(&self, req: ServiceRequest, srv: &mut S) -> Result {
let res = srv.call(req).await?;
// Check response body for leakage patterns
let body = res.body();
if let Ok(bytes) = body.as_bytes() {
let body_str = String::from_utf8_lossy(bytes);
if contains_sensitive_patterns(&body_str) {
log::warn!("LLM data leakage detected in response: {}", body_str);
}
}
Ok(res)
}
}
Actix-Specific Remediation
Remediating LLM data leakage in Actix requires implementing proper authentication, response sanitization, and error handling. Start with authentication middleware that protects all LLM endpoints:
use actix_web::{dev::ServiceRequest, guard, web, HttpResponse};
use actix_web_httpauth::middleware::HttpAuthentication;
async fn llm_auth(req: ServiceRequest) -> Result {
// Check for valid API key or JWT
if let Some(auth_header) = req.headers().get("Authorization") {
let token = auth_header.to_str().unwrap_or("");
if validate_api_key(token) || validate_jwt(token) {
return Ok(req);
}
}
// Block unauthenticated access to LLM endpoints
let path = req.path();
if path.contains("chat") || path.contains("llm") || path.contains("model") {
return Err(actix_web::error::ErrorUnauthorized("LLM access requires authentication"));
}
Ok(req)
}
// Apply to all routes
app.wrap(middleware::NormalizePath::trim())
.wrap(middleware::Logger::default())
.wrap(HttpAuthentication::bearer(llm_auth));
For response sanitization, create a filter that removes sensitive information from LLM outputs:
use actix_web::{dev::ServiceResponse, Error};
use bytes::Bytes;
pub fn sanitize_llm_response(
mut response: ServiceResponse,
) -> Result {
let body = response.body_mut();
if let Ok(bytes) = body.as_bytes() {
let body_str = String::from_utf8_lossy(bytes);
// Remove system prompts and sensitive patterns
let sanitized = body_str
.replace("SYSTEM", "[REDACTED SYSTEM PROMPT]")
.replace("Assistant will", "[REDACTED INSTRUCTIONS]")
.replace("Training data:", "[REDACTED TRAINING DATA]");
// Replace body with sanitized version
let new_body = Bytes::from(sanitized.as_bytes().to_vec());
response = response.map_body(|_, _| new_body);
}
Ok(response)
}
// Apply as middleware
app.wrap(sanitize_llm_response);
Implement proper error boundaries in Actix handlers to prevent stack trace leakage:
async fn chat_model(
req: HttpRequest,
payload: actix_web::web::Payload,
) -> impl Responder {
let body = actix_web::web::Bytes::from(req.clone().into_body()).await?;
let prompt = String::from_utf8(body.to_vec())?;
// Error boundary with safe fallback
let response = match llm_model.generate(prompt).await {
Ok(res) => res,
Err(e) => {
log::error!("LLM generation failed: {}", e);
"[SERVICE UNAVAILABLE]"
}
};
HttpResponse::Ok().json({
"response": response,
})
}
For streaming responses, implement output filtering before sending data to clients:
async fn chat_stream(
req: HttpRequest,
payload: actix_web::web::Payload,
) -> impl Responder {
let prompt = extract_prompt(req).await?;
let mut response = HttpResponse::Ok()
.content_type("text/plain")
.streaming({
async_stream::stream! {
for chunk in llm_model.generate_stream(prompt).await {
// Filter sensitive content in real-time
let sanitized = sanitize_chunk(chunk);
yield sanitized;
}
}
});
response
}
fn sanitize_chunk(chunk: String) -> String {
chunk
.replace("SYSTEM", "[REDACTED]")
.replace("Training data", "[REDACTED DATA]")
.replace("Model version", "[REDACTED VERSION]")
}
Finally, implement rate limiting on LLM endpoints to prevent abuse and data extraction:
use actix_web::{guard, web, HttpResponse};
use actix_ratelimit::RateLimiter;
let api_rate_limiter = RateLimiter::middleware(
ratelimit::RateLimit::key_by(|req: &ServiceRequest| {
req.headers().get("x-api-key").cloned()
})
.limit(100) // 100 requests per period
.period(std::time::Duration::from_secs(3600))
.forward_header("x-ratelimit-remaining")
.forward_header("x-ratelimit-reset")
);
// Apply to LLM routes
app.service(
web::resource("/chat")
.guard(guard::post())
.wrap(api_rate_limiter)
.route(web::post().to(chat_model))
);
Related CWEs: llmSecurity
| CWE ID | Name | Severity |
|---|---|---|
| CWE-754 | Improper Check for Unusual or Exceptional Conditions | MEDIUM |