Skip to content

Commit 715ee3c

Browse files
committed
enhance app security
1 parent 5889f69 commit 715ee3c

3 files changed

Lines changed: 458 additions & 1 deletion

File tree

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@ ReadME_DEPLOYMENT.md
6868
CHANGES.md
6969
SECURITY.md
7070
LOCAL_CLIENT_SETUP.md
71-
SECURITY_ASSESSMENT.md
71+
SECURITY_ASSESSMENT.md
72+
Security_Risk_clarification.md
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
"""
2+
Security Service
3+
4+
Provides security utilities including:
5+
- Prompt injection detection
6+
- Rate limiting
7+
- Audit logging
8+
- Content validation
9+
"""
10+
11+
import re
12+
import time
13+
from collections import defaultdict
14+
from datetime import datetime, timedelta
15+
from typing import Dict, Optional, Tuple, List
16+
from pathlib import Path
17+
from fastmcp.utilities.logging import get_logger
18+
19+
logger = get_logger("SecurityService")
20+
21+
22+
class SecurityService:
23+
"""
24+
Security service for detecting threats and enforcing rate limits.
25+
"""
26+
27+
def __init__(self, data_dir: Path):
28+
"""
29+
Initialize security service.
30+
31+
Args:
32+
data_dir: Directory for storing security logs
33+
"""
34+
self.logger = get_logger("SecurityService")
35+
self.data_dir = data_dir
36+
self.audit_log_dir = data_dir / "audit_logs"
37+
self.audit_log_dir.mkdir(parents=True, exist_ok=True)
38+
39+
# Rate limiting: track requests per user_id
40+
self.rate_limit_store: Dict[str, List[float]] = defaultdict(list)
41+
42+
# Rate limit configuration
43+
self.rate_limits = {
44+
'read': {'max_requests': 100, 'window_seconds': 60}, # 100 reads per minute
45+
'write': {'max_requests': 20, 'window_seconds': 60}, # 20 writes per minute
46+
'general': {'max_requests': 200, 'window_seconds': 60} # 200 general requests per minute
47+
}
48+
49+
# Prompt injection patterns (common attack patterns)
50+
self.injection_patterns = [
51+
r'(?i)ignore\s+(all\s+)?previous\s+instructions?',
52+
r'(?i)forget\s+(all\s+)?previous\s+instructions?',
53+
r'(?i)disregard\s+(all\s+)?previous\s+instructions?',
54+
r'(?i)system\s*:\s*',
55+
r'(?i)assistant\s*:\s*',
56+
r'(?i)you\s+are\s+now\s+',
57+
r'(?i)new\s+instructions?\s*:',
58+
r'(?i)override\s+instructions?',
59+
r'(?i)new\s+system\s+prompt',
60+
r'(?i)act\s+as\s+if\s+you\s+are',
61+
r'(?i)pretend\s+to\s+be',
62+
r'(?i)roleplay\s+as',
63+
r'(?i)execute\s+this\s+command',
64+
r'(?i)run\s+this\s+code',
65+
r'(?i)send\s+(this\s+)?(data|information|content)\s+to',
66+
r'(?i)exfiltrate',
67+
r'(?i)leak\s+(this\s+)?(data|information)',
68+
r'(?i)reveal\s+(this\s+)?(data|information)',
69+
]
70+
71+
# Suspicious write patterns
72+
self.suspicious_write_patterns = [
73+
r'(?i)delete\s+all',
74+
r'(?i)clear\s+everything',
75+
r'(?i)remove\s+all\s+content',
76+
r'(?i)overwrite\s+with\s+nothing',
77+
r'(?i)^\s*$', # Empty or whitespace-only content
78+
r'(?i)password|api[_\s]?key|secret|token|credential',
79+
]
80+
81+
def detect_prompt_injection(self, content: str) -> Tuple[bool, List[str]]:
82+
"""
83+
Detect potential prompt injection attacks in content.
84+
85+
Args:
86+
content: Content to analyze
87+
88+
Returns:
89+
Tuple of (is_suspicious, matched_patterns)
90+
"""
91+
if not content:
92+
return False, []
93+
94+
matched_patterns = []
95+
content_lower = content.lower()
96+
97+
for pattern in self.injection_patterns:
98+
if re.search(pattern, content):
99+
matched_patterns.append(pattern)
100+
101+
is_suspicious = len(matched_patterns) > 0
102+
103+
if is_suspicious:
104+
self.logger.warning(
105+
f"Potential prompt injection detected. Matched {len(matched_patterns)} patterns."
106+
)
107+
108+
return is_suspicious, matched_patterns
109+
110+
def validate_write_content(self, content: str, doc_id: str, user_id: str) -> Tuple[bool, Optional[str]]:
111+
"""
112+
Validate write content for suspicious patterns.
113+
114+
Args:
115+
content: Content to be written
116+
doc_id: Document ID
117+
user_id: User ID
118+
119+
Returns:
120+
Tuple of (is_valid, error_message)
121+
"""
122+
if not content:
123+
return False, "Content cannot be empty"
124+
125+
# Check for suspicious patterns
126+
for pattern in self.suspicious_write_patterns:
127+
if re.search(pattern, content):
128+
self.log_audit_event(
129+
'write_validation_failed',
130+
user_id,
131+
{
132+
'reason': 'suspicious_pattern',
133+
'pattern': pattern,
134+
'doc_id': doc_id,
135+
'content_preview': content[:100]
136+
}
137+
)
138+
return False, f"Suspicious content pattern detected. Please review your content."
139+
140+
# Check for prompt injection
141+
is_injection, patterns = self.detect_prompt_injection(content)
142+
if is_injection:
143+
self.log_audit_event(
144+
'write_validation_failed',
145+
user_id,
146+
{
147+
'reason': 'prompt_injection',
148+
'patterns': patterns,
149+
'doc_id': doc_id,
150+
'content_preview': content[:100]
151+
}
152+
)
153+
return False, "Potential prompt injection detected in content. Please review."
154+
155+
# Check content size (already done in main code, but double-check)
156+
if len(content) > 50000:
157+
return False, "Content too large"
158+
159+
return True, None
160+
161+
def check_rate_limit(self, user_id: str, operation_type: str = 'general') -> Tuple[bool, Optional[str]]:
162+
"""
163+
Check if user has exceeded rate limits.
164+
165+
Args:
166+
user_id: User identifier
167+
operation_type: Type of operation ('read', 'write', 'general')
168+
169+
Returns:
170+
Tuple of (is_allowed, error_message)
171+
"""
172+
if operation_type not in self.rate_limits:
173+
operation_type = 'general'
174+
175+
limit_config = self.rate_limits[operation_type]
176+
max_requests = limit_config['max_requests']
177+
window_seconds = limit_config['window_seconds']
178+
179+
now = time.time()
180+
key = f"{user_id}:{operation_type}"
181+
182+
# Clean old entries
183+
self.rate_limit_store[key] = [
184+
timestamp for timestamp in self.rate_limit_store[key]
185+
if now - timestamp < window_seconds
186+
]
187+
188+
# Check limit
189+
if len(self.rate_limit_store[key]) >= max_requests:
190+
self.log_audit_event(
191+
'rate_limit_exceeded',
192+
user_id,
193+
{
194+
'operation_type': operation_type,
195+
'requests': len(self.rate_limit_store[key]),
196+
'limit': max_requests,
197+
'window_seconds': window_seconds
198+
}
199+
)
200+
return False, f"Rate limit exceeded. Maximum {max_requests} {operation_type} operations per {window_seconds} seconds."
201+
202+
# Record this request
203+
self.rate_limit_store[key].append(now)
204+
205+
return True, None
206+
207+
def log_audit_event(
208+
self,
209+
event_type: str,
210+
user_id: str,
211+
details: Dict,
212+
severity: str = 'info'
213+
):
214+
"""
215+
Log security audit event.
216+
217+
Args:
218+
event_type: Type of event (e.g., 'rate_limit_exceeded', 'prompt_injection_detected')
219+
user_id: User identifier
220+
details: Additional event details
221+
severity: Event severity ('info', 'warning', 'error', 'critical')
222+
"""
223+
timestamp = datetime.utcnow().isoformat()
224+
225+
log_entry = {
226+
'timestamp': timestamp,
227+
'event_type': event_type,
228+
'user_id': user_id,
229+
'severity': severity,
230+
'details': details
231+
}
232+
233+
# Log to console
234+
log_message = f"[AUDIT] {event_type} | user={user_id} | {details}"
235+
if severity == 'critical' or severity == 'error':
236+
self.logger.error(log_message)
237+
elif severity == 'warning':
238+
self.logger.warning(log_message)
239+
else:
240+
self.logger.info(log_message)
241+
242+
# Write to audit log file (daily rotation)
243+
today = datetime.utcnow().date()
244+
log_file = self.audit_log_dir / f"audit_{today.isoformat()}.jsonl"
245+
246+
try:
247+
import json
248+
with open(log_file, 'a') as f:
249+
f.write(json.dumps(log_entry) + '\n')
250+
except Exception as e:
251+
self.logger.error(f"Failed to write audit log: {e}")
252+
253+
def sanitize_content_for_logging(self, content: str, max_length: int = 100) -> str:
254+
"""
255+
Sanitize content for safe logging (remove sensitive data).
256+
257+
Args:
258+
content: Content to sanitize
259+
max_length: Maximum length to log
260+
261+
Returns:
262+
Sanitized content preview
263+
"""
264+
if not content:
265+
return ""
266+
267+
# Remove potential sensitive patterns
268+
sanitized = re.sub(r'(?i)(password|api[_\s]?key|secret|token|credential)\s*[:=]\s*\S+',
269+
r'\1=***REDACTED***', content)
270+
271+
# Truncate
272+
if len(sanitized) > max_length:
273+
sanitized = sanitized[:max_length] + "..."
274+
275+
return sanitized
276+
277+
def get_security_warning_for_content(self, content: str) -> Optional[str]:
278+
"""
279+
Get a security warning message if content contains suspicious patterns.
280+
281+
Args:
282+
content: Content to check
283+
284+
Returns:
285+
Warning message or None
286+
"""
287+
is_injection, patterns = self.detect_prompt_injection(content)
288+
289+
if is_injection:
290+
return (
291+
"⚠️ SECURITY WARNING: This content contains patterns that may be "
292+
"attempting to inject instructions. Please review carefully before proceeding."
293+
)
294+
295+
return None
296+

0 commit comments

Comments
 (0)