-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsecurity.py
More file actions
357 lines (302 loc) · 12.1 KB
/
Copy pathsecurity.py
File metadata and controls
357 lines (302 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""
Security utilities for ScriptAI
Handles input validation, sanitization, and rate limiting
"""
import re
import time
import hashlib
import hmac
import os
from typing import Dict, List, Optional, Tuple
from collections import defaultdict, deque
import html
import logging
class SecurityManager:
"""Manages security features for ScriptAI"""
def __init__(
self,
max_prompt_length: int = 1000,
rate_limit: int = 100,
signing_secret: Optional[str] = None,
):
self.max_prompt_length = max_prompt_length
self.rate_limit = rate_limit
self.rate_limit_window = 3600 # 1 hour in seconds
self.request_counts: Dict[str, deque] = defaultdict(lambda: deque())
# Signature verification configuration
self.signature_scheme = "v1"
self.signature_header = "X-Signature"
self.signature_timestamp_header = "X-Signature-Timestamp"
# Resolve signing secret from argument or environment
self.signing_secret = signing_secret or os.getenv(
"REQUEST_SIGNATURE_SECRET", os.getenv("SIGNING_SECRET")
)
# Dangerous patterns to detect
self.dangerous_patterns = [
r"<script[^>]*>.*?</script>", # Script tags
r"javascript:", # JavaScript URLs
r"data:text/html", # Data URLs
r"vbscript:", # VBScript
r"on\w+\s*=", # Event handlers
r"<iframe[^>]*>", # Iframes
r"<object[^>]*>", # Objects
r"<embed[^>]*>", # Embeds
]
# Compile patterns for efficiency
self.compiled_patterns = [
re.compile(pattern, re.IGNORECASE) for pattern in self.dangerous_patterns
]
# Structured logger (configured by MonitoringManager)
self.logger = logging.getLogger("ScriptAI.Security")
def validate_prompt(self, prompt: str) -> Tuple[bool, Optional[str]]:
"""
Validate user prompt for security issues
Args:
prompt: User input prompt
Returns:
Tuple of (is_valid, error_message)
"""
if not prompt or not prompt.strip():
return False, "Empty prompt not allowed"
if len(prompt) > self.max_prompt_length:
return (
False,
f"Prompt too long. Maximum {self.max_prompt_length} characters allowed",
)
# Check for dangerous patterns
for pattern in self.compiled_patterns:
if pattern.search(prompt):
return False, "Potentially dangerous content detected"
# Check for excessive repetition (potential spam)
words = prompt.lower().split()
if len(words) > 10:
word_counts: Dict[str, int] = {}
for word in words:
word_counts[word] = word_counts.get(word, 0) + 1
if word_counts[word] > len(words) * 0.3: # More than 30% repetition
return False, "Excessive repetition detected"
return True, None
def sanitize_input(self, text: str) -> str:
"""
Sanitize user input to prevent XSS and other attacks
Args:
text: Input text to sanitize
Returns:
Sanitized text
"""
# HTML escape
sanitized = html.escape(text)
# Remove potential script injections
sanitized = re.sub(
r"<script[^>]*>.*?</script>", "", sanitized, flags=re.IGNORECASE | re.DOTALL
)
sanitized = re.sub(r"javascript:", "", sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r"vbscript:", "", sanitized, flags=re.IGNORECASE)
return sanitized.strip()
def check_rate_limit(self, client_ip: str) -> Tuple[bool, Optional[str]]:
"""
Check if client has exceeded rate limit
Args:
client_ip: Client IP address
Returns:
Tuple of (within_limit, error_message)
"""
current_time = time.time()
# Clean old requests outside the window
while (
self.request_counts[client_ip]
and self.request_counts[client_ip][0]
< current_time - self.rate_limit_window
):
self.request_counts[client_ip].popleft()
# Check if within rate limit
if len(self.request_counts[client_ip]) >= self.rate_limit:
return (
False,
f"Rate limit exceeded. Maximum {self.rate_limit} requests per hour",
)
# Add current request
self.request_counts[client_ip].append(current_time)
return True, None
def validate_api_key(self, api_key: str) -> bool:
"""
Validate API key format (basic validation)
Args:
api_key: API key to validate
Returns:
True if valid format
"""
if not api_key or len(api_key) < 10:
return False
# Check for common patterns
if api_key.startswith("sk-") and len(api_key) > 20: # OpenAI format
return True
if api_key.startswith("hf_") and len(api_key) > 20: # HuggingFace format
return True
if len(api_key) > 20: # Generic long key
return True
return False
def log_security_event(
self, event_type: str, details: str, client_ip: Optional[str] = None
):
"""
Log security events using structured logging.
Args:
event_type: Type of security event
details: Event details
client_ip: Client IP if available
"""
try:
self.logger.warning(
event_type,
extra={
"message": details,
"client_ip": client_ip,
},
)
except Exception:
# Never fail due to logging issues
pass
def get_security_stats(self) -> Dict:
"""
Get security statistics
Returns:
Dictionary with security stats
"""
current_time = time.time()
active_clients = 0
for client_ip, requests in self.request_counts.items():
# Count clients with recent requests
recent_requests = [req for req in requests if req > current_time - 3600]
if recent_requests:
active_clients += 1
return {
"active_clients": active_clients,
"total_requests_last_hour": sum(
len(requests) for requests in self.request_counts.values()
),
"rate_limit": self.rate_limit,
"max_prompt_length": self.max_prompt_length,
}
# --- Signature-based request verification ---
def set_signing_secret(self, secret: Optional[str]) -> None:
"""
Set or update the signing secret used for HMAC verification.
"""
self.signing_secret = secret
def is_signature_configured(self) -> bool:
"""
Returns True if a signing secret is configured.
"""
return bool(self.signing_secret)
def _compute_signature(
self, body: bytes, timestamp: str, scheme: Optional[str] = None
) -> str:
"""
Compute the HMAC SHA256 hex digest for the given payload and timestamp.
The base string is: "{scheme}:{timestamp}:{body}" and the returned header
value format is "{scheme}={hexdigest}".
"""
used_scheme = (scheme or self.signature_scheme).encode("utf-8")
base = used_scheme + b":" + timestamp.encode("utf-8") + b":" + body
digest = hmac.new(
(self.signing_secret or "").encode("utf-8"), base, hashlib.sha256
).hexdigest()
return f"{(scheme or self.signature_scheme)}={digest}"
def sign_payload(
self, body: bytes, timestamp: Optional[int] = None, scheme: Optional[str] = None
) -> str:
"""
Produce an HMAC signature header value for a request body.
Returns a string like "v1=<hex>". If no timestamp is provided, current
epoch seconds are used.
"""
ts = str(int(timestamp if timestamp is not None else time.time()))
return self._compute_signature(body, ts, scheme)
def verify_request_signature(
self,
headers: Dict[str, str],
body: bytes,
client_ip: Optional[str] = None,
tolerance_seconds: int = 300,
require: bool = False,
) -> Tuple[bool, Optional[str]]:
"""
Verify HMAC-based request signature.
- Expects headers `X-Signature` (e.g., "v1=<hex>") and `X-Signature-Timestamp` (epoch seconds).
- Uses `{scheme}:{timestamp}:{body}` as the message for HMAC SHA256.
- Returns (True, None) if signature is valid, or if not configured and `require` is False.
Args:
headers: Request headers mapping (case-insensitive where possible).
body: Raw request body bytes used for signature calculation.
client_ip: Optional client IP for logging.
tolerance_seconds: Allowed clock skew for timestamp verification.
require: If True, fail when signature or secret is missing/invalid.
"""
# Resolve secret at verification time to allow env changes without restart
if not self.signing_secret:
self.signing_secret = os.getenv(
"REQUEST_SIGNATURE_SECRET", os.getenv("SIGNING_SECRET")
)
# If signature verification isn't configured, allow unless strictly required
if not self.signing_secret:
if require:
msg = "Signature required but no signing secret configured"
self.log_security_event("signature_missing_secret", msg, client_ip)
return False, msg
return True, None
# Extract headers
sig_header = headers.get(self.signature_header) or headers.get(
self.signature_header.lower()
)
ts_header = headers.get(self.signature_timestamp_header) or headers.get(
self.signature_timestamp_header.lower()
)
if not sig_header:
msg = "Missing X-Signature header"
if require:
self.log_security_event("signature_missing", msg, client_ip)
return False, msg
# If not required, skip verification
return True, None
if not ts_header:
msg = "Missing X-Signature-Timestamp header"
self.log_security_event("signature_missing_timestamp", msg, client_ip)
return False, msg
# Parse signature header scheme=value
try:
if "=" in sig_header:
scheme, provided_digest = sig_header.split("=", 1)
else:
scheme, provided_digest = self.signature_scheme, sig_header
scheme = scheme.strip()
provided_digest = provided_digest.strip()
except Exception:
msg = "Invalid X-Signature format"
self.log_security_event("signature_bad_format", msg, client_ip)
return False, msg
# Validate timestamp window
try:
ts_int = int(ts_header)
except ValueError:
msg = "Invalid X-Signature-Timestamp (must be epoch seconds)"
self.log_security_event("signature_bad_timestamp", msg, client_ip)
return False, msg
now = int(time.time())
if abs(now - ts_int) > tolerance_seconds:
msg = "Signature timestamp outside allowable window"
self.log_security_event("signature_timestamp_out_of_window", msg, client_ip)
return False, msg
# Compute expected digest and compare
expected = self._compute_signature(body, str(ts_int), scheme)
try:
expected_digest = expected.split("=", 1)[1]
except Exception:
msg = "Internal signature computation error"
self.log_security_event("signature_internal_error", msg, client_ip)
return False, msg
if not hmac.compare_digest(provided_digest, expected_digest):
msg = "Signature mismatch"
self.log_security_event("signature_mismatch", msg, client_ip)
return False, msg
return True, None