-
-
Notifications
You must be signed in to change notification settings - Fork 1
Security and Filtering
Rumen Damyanov edited this page Jul 31, 2025
·
1 revision
Comprehensive guide to security features, message filtering, and abuse prevention.
- Security Overview
- Message Filtering
- Rate Limiting
- Input Validation
- API Key Security
- Content Filtering
- Abuse Prevention
- Logging and Monitoring
PHP Chatbot includes multiple layers of security to protect against abuse, ensure safe operation, and maintain data privacy.
| Layer | Purpose | Implementation |
|---|---|---|
| Input Validation | Prevent malicious input | Length limits, format validation |
| Rate Limiting | Prevent spam/abuse | IP-based request throttling |
| Content Filtering | Block inappropriate content | Profanity, link, pattern filtering |
| API Security | Protect credentials | Key rotation, encryption |
| Output Sanitization | Safe response delivery | XSS prevention, encoding |
| Audit Logging | Track usage and issues | Comprehensive logging system |
// Default security configuration
return [
'security' => [
'rate_limit' => 10, // Requests per minute
'max_input_length' => 2000, // Character limit
'enable_filtering' => true, // Content filtering
'log_conversations' => true, // Audit logging
'sanitize_output' => true, // XSS prevention
'encrypt_api_keys' => true, // Key encryption
]
];The built-in ChatMessageFilterMiddleware provides configurable content filtering.
'message_filtering' => [
'enabled' => true,
// System instructions (hidden from users)
'instructions' => [
'Use appropriate and professional language.',
'Avoid sharing external links or URLs.',
'Reject requests for harmful or illegal content.',
'De-escalate aggressive or rude interactions.',
'Stay focused on helping users with legitimate questions.',
],
// Profanity filter
'profanities' => [
'spam', 'scam', 'fraud', 'fake',
// Add your custom blocked words
],
// Aggression patterns
'aggression_patterns' => [
'hate', 'kill', 'die', 'stupid', 'idiot',
'shut up', 'go away', 'f*ck', 'damn',
],
// Link detection pattern
'link_pattern' => '/https?:\/\/[\w\.-]+/i',
// Actions on violations
'action_on_profanity' => 'reject', // reject, warn, modify
'action_on_aggression' => 'warn', // reject, warn, modify
'action_on_links' => 'modify', // reject, warn, modify
],use Rumenx\PhpChatbot\Middleware\ChatMessageFilterMiddleware;
use Rumenx\PhpChatbot\PhpChatbot;
// Load configuration
$config = require 'config/phpchatbot.php';
$filterConfig = $config['message_filtering'] ?? [];
// Create filter middleware
$middleware = new ChatMessageFilterMiddleware(
$filterConfig['instructions'] ?? [],
$filterConfig['profanities'] ?? [],
$filterConfig['aggression_patterns'] ?? [],
$filterConfig['link_pattern'] ?? ''
);
// Apply filtering before AI processing
$userMessage = "Hello, can you help me with something?";
$context = ['user_id' => 123, 'ip' => '192.168.1.1'];
$filtered = $middleware->handle($userMessage, $context);
// Send to AI model
$chatbot = new PhpChatbot($model, $config);
$response = $chatbot->ask($filtered['message'], $filtered['context']);class CustomMessageFilter extends ChatMessageFilterMiddleware
{
protected function customFilters(string $message): array
{
$violations = [];
// Check for personal information
if (preg_match('/\b\d{3}-\d{2}-\d{4}\b/', $message)) {
$violations[] = 'social_security_number';
}
// Check for credit card numbers
if (preg_match('/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/', $message)) {
$violations[] = 'credit_card';
}
// Check for email addresses
if (preg_match('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', $message)) {
$violations[] = 'email_address';
}
return $violations;
}
protected function handleCustomViolation(string $violation, string $message): string
{
switch ($violation) {
case 'social_security_number':
case 'credit_card':
return "I can't process messages containing sensitive personal information.";
case 'email_address':
return preg_replace('/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/', '[EMAIL REDACTED]', $message);
default:
return $message;
}
}
}Protect against spam and abuse with flexible rate limiting.
class SimpleRateLimiter
{
private $storage;
private $limit;
private $window;
public function __construct(int $limit = 10, int $window = 60)
{
$this->limit = $limit;
$this->window = $window;
$this->storage = sys_get_temp_dir() . '/chatbot_rates/';
if (!is_dir($this->storage)) {
mkdir($this->storage, 0755, true);
}
}
public function isAllowed(string $identifier): bool
{
$file = $this->storage . md5($identifier);
$now = time();
if (!file_exists($file)) {
file_put_contents($file, json_encode([$now]));
return true;
}
$requests = json_decode(file_get_contents($file), true);
$requests = array_filter($requests, fn($time) => $now - $time < $this->window);
if (count($requests) >= $this->limit) {
return false;
}
$requests[] = $now;
file_put_contents($file, json_encode($requests));
return true;
}
public function getRemainingRequests(string $identifier): int
{
$file = $this->storage . md5($identifier);
if (!file_exists($file)) {
return $this->limit;
}
$requests = json_decode(file_get_contents($file), true);
$now = time();
$recent = array_filter($requests, fn($time) => $now - $time < $this->window);
return max(0, $this->limit - count($recent));
}
}class AdvancedRateLimiter
{
private $redis;
private $configs;
public function __construct($redis, array $configs)
{
$this->redis = $redis;
$this->configs = $configs;
}
public function checkLimit(string $identifier, string $type = 'default'): array
{
$config = $this->configs[$type] ?? $this->configs['default'];
$key = "rate_limit:{$type}:{$identifier}";
$limits = [
'minute' => $config['per_minute'] ?? 10,
'hour' => $config['per_hour'] ?? 100,
'day' => $config['per_day'] ?? 1000,
];
$windows = [
'minute' => 60,
'hour' => 3600,
'day' => 86400,
];
foreach ($limits as $period => $limit) {
$periodKey = "{$key}:{$period}";
$window = $windows[$period];
$current = $this->redis->get($periodKey) ?: 0;
if ($current >= $limit) {
return [
'allowed' => false,
'limit' => $limit,
'remaining' => 0,
'reset_time' => time() + $window,
'period' => $period,
];
}
}
// Increment counters
foreach ($windows as $period => $window) {
$periodKey = "{$key}:{$period}";
$this->redis->incr($periodKey);
$this->redis->expire($periodKey, $window);
}
return [
'allowed' => true,
'limit' => $limits['minute'],
'remaining' => $limits['minute'] - ($this->redis->get("{$key}:minute") ?: 0),
'reset_time' => time() + 60,
];
}
}// In RouteServiceProvider or middleware
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('chatbot', function ($request) {
return [
Limit::perMinute(10)->by($request->ip()),
Limit::perHour(100)->by($request->ip()),
Limit::perDay(1000)->by($request->ip()),
];
});
// In routes
Route::post('/chatbot/message', [ChatbotController::class, 'message'])
->middleware('throttle:chatbot');# config/packages/rate_limiter.yaml
framework:
rate_limiter:
chatbot:
policy: 'sliding_window'
limit: 10
interval: '1 minute'
chatbot_heavy:
policy: 'token_bucket'
limit: 100
rate: { interval: '1 hour', amount: 100 }Comprehensive input validation prevents malicious data from reaching the AI.
class InputValidator
{
private array $config;
public function __construct(array $config = [])
{
$this->config = array_merge([
'max_length' => 2000,
'min_length' => 1,
'allowed_chars' => '/^[\p{L}\p{N}\p{P}\p{Z}\s]+$/u',
'blocked_patterns' => [],
'sanitize_html' => true,
], $config);
}
public function validate(string $input): array
{
$errors = [];
$sanitized = $this->sanitizeInput($input);
// Length validation
if (strlen($sanitized) < $this->config['min_length']) {
$errors[] = 'Input too short';
}
if (strlen($sanitized) > $this->config['max_length']) {
$errors[] = 'Input too long';
}
// Character validation
if (!preg_match($this->config['allowed_chars'], $sanitized)) {
$errors[] = 'Contains invalid characters';
}
// Pattern blocking
foreach ($this->config['blocked_patterns'] as $pattern) {
if (preg_match($pattern, $sanitized)) {
$errors[] = 'Contains blocked content';
break;
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'sanitized' => $sanitized,
'original' => $input,
];
}
private function sanitizeInput(string $input): string
{
// Remove null bytes
$input = str_replace("\0", '', $input);
// Normalize whitespace
$input = preg_replace('/\s+/', ' ', $input);
// Remove HTML if configured
if ($this->config['sanitize_html']) {
$input = strip_tags($input);
}
// Normalize Unicode
if (function_exists('normalizer_normalize')) {
$input = normalizer_normalize($input, Normalizer::FORM_C);
}
return trim($input);
}
}class AdvancedInputValidator extends InputValidator
{
public function validateSecurity(string $input): array
{
$issues = [];
// SQL injection patterns
$sqlPatterns = [
'/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\b)/i',
'/(\bunion\s+select\b)/i',
'/(\b(OR|AND)\s+\d+\s*=\s*\d+)/i',
];
foreach ($sqlPatterns as $pattern) {
if (preg_match($pattern, $input)) {
$issues[] = 'potential_sql_injection';
break;
}
}
// XSS patterns
$xssPatterns = [
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi',
'/javascript:/i',
'/on\w+\s*=/i',
];
foreach ($xssPatterns as $pattern) {
if (preg_match($pattern, $input)) {
$issues[] = 'potential_xss';
break;
}
}
// Command injection
$cmdPatterns = [
'/[;&|`$(){}[\]]/i',
'/\b(cat|ls|rm|wget|curl|nc|netcat)\b/i',
];
foreach ($cmdPatterns as $pattern) {
if (preg_match($pattern, $input)) {
$issues[] = 'potential_command_injection';
break;
}
}
return $issues;
}
}Protect AI provider API keys with encryption and rotation.
class ApiKeyManager
{
private string $encryptionKey;
private string $cipher = 'AES-256-GCM';
public function __construct(string $encryptionKey = null)
{
$this->encryptionKey = $encryptionKey ?: $this->generateKey();
}
public function encryptKey(string $apiKey): string
{
$iv = random_bytes(12); // GCM requires 12 bytes IV
$tag = '';
$encrypted = openssl_encrypt(
$apiKey,
$this->cipher,
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv,
$tag
);
return base64_encode($iv . $tag . $encrypted);
}
public function decryptKey(string $encryptedKey): string
{
$data = base64_decode($encryptedKey);
$iv = substr($data, 0, 12);
$tag = substr($data, 12, 16);
$encrypted = substr($data, 28);
$decrypted = openssl_decrypt(
$encrypted,
$this->cipher,
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv,
$tag
);
if ($decrypted === false) {
throw new \Exception('Failed to decrypt API key');
}
return $decrypted;
}
private function generateKey(): string
{
return random_bytes(32); // 256 bits
}
}class ApiKeyRotator
{
private $storage;
private ApiKeyManager $keyManager;
public function __construct($storage, ApiKeyManager $keyManager)
{
$this->storage = $storage;
$this->keyManager = $keyManager;
}
public function rotateKey(string $provider, string $newKey): void
{
// Backup current key
$currentKey = $this->getCurrentKey($provider);
if ($currentKey) {
$this->backupKey($provider, $currentKey);
}
// Store new encrypted key
$encryptedKey = $this->keyManager->encryptKey($newKey);
$this->storage->put("api_keys.{$provider}", $encryptedKey);
// Log rotation
$this->logKeyRotation($provider);
}
public function getCurrentKey(string $provider): ?string
{
$encryptedKey = $this->storage->get("api_keys.{$provider}");
if (!$encryptedKey) {
return null;
}
return $this->keyManager->decryptKey($encryptedKey);
}
private function backupKey(string $provider, string $key): void
{
$backup = [
'provider' => $provider,
'key_hash' => hash('sha256', $key),
'rotated_at' => time(),
];
$this->storage->put("api_key_backups.{$provider}." . time(), $backup);
}
private function logKeyRotation(string $provider): void
{
error_log("API key rotated for provider: {$provider} at " . date('Y-m-d H:i:s'));
}
}Advanced content filtering for various types of inappropriate content.
class ContentFilter
{
private array $filters;
public function __construct()
{
$this->filters = [
'profanity' => new ProfanityFilter(),
'toxicity' => new ToxicityFilter(),
'spam' => new SpamFilter(),
'personal_info' => new PersonalInfoFilter(),
];
}
public function filter(string $content, array $options = []): array
{
$result = [
'original' => $content,
'filtered' => $content,
'violations' => [],
'score' => 0,
'action' => 'allow',
];
foreach ($this->filters as $name => $filter) {
if (!($options[$name] ?? true)) {
continue; // Skip disabled filters
}
$filterResult = $filter->check($result['filtered']);
if ($filterResult['violations']) {
$result['violations'] = array_merge(
$result['violations'],
$filterResult['violations']
);
$result['score'] += $filterResult['severity'];
$result['filtered'] = $filterResult['content'];
}
}
// Determine action based on score
if ($result['score'] >= 80) {
$result['action'] = 'block';
} elseif ($result['score'] >= 50) {
$result['action'] = 'warn';
} elseif ($result['score'] >= 20) {
$result['action'] = 'flag';
}
return $result;
}
}
class ProfanityFilter
{
private array $words;
public function __construct()
{
$this->words = [
'mild' => ['damn', 'hell', 'crap'],
'moderate' => ['shit', 'bitch', 'ass'],
'severe' => ['fuck', 'cunt', 'nigger'],
];
}
public function check(string $content): array
{
$violations = [];
$severity = 0;
$filtered = $content;
foreach ($this->words as $level => $words) {
$levelSeverity = match($level) {
'mild' => 10,
'moderate' => 30,
'severe' => 80,
};
foreach ($words as $word) {
$pattern = '/\b' . preg_quote($word, '/') . '\b/i';
if (preg_match($pattern, $content)) {
$violations[] = [
'type' => 'profanity',
'level' => $level,
'word' => $word,
];
$severity = max($severity, $levelSeverity);
$filtered = preg_replace($pattern, str_repeat('*', strlen($word)), $filtered);
}
}
}
return [
'violations' => $violations,
'severity' => $severity,
'content' => $filtered,
];
}
}Comprehensive abuse prevention system.
class AbuseDetector
{
private $storage;
private array $rules;
public function __construct($storage, array $rules = [])
{
$this->storage = $storage;
$this->rules = array_merge([
'repeated_messages' => ['limit' => 3, 'window' => 300, 'score' => 30],
'rapid_requests' => ['limit' => 20, 'window' => 60, 'score' => 40],
'long_sessions' => ['limit' => 100, 'window' => 3600, 'score' => 20],
'pattern_spam' => ['pattern' => '/(.)\1{10,}/', 'score' => 50],
], $rules);
}
public function checkAbuse(string $identifier, array $context = []): array
{
$score = 0;
$violations = [];
// Check each rule
foreach ($this->rules as $rule => $config) {
$violation = $this->checkRule($rule, $identifier, $context, $config);
if ($violation) {
$violations[] = $violation;
$score += $config['score'];
}
}
// Determine action
$action = match(true) {
$score >= 100 => 'ban',
$score >= 70 => 'temporary_ban',
$score >= 40 => 'rate_limit',
$score >= 20 => 'warning',
default => 'allow'
};
return [
'score' => $score,
'violations' => $violations,
'action' => $action,
'ban_duration' => $this->calculateBanDuration($score),
];
}
private function checkRule(string $rule, string $identifier, array $context, array $config): ?array
{
switch ($rule) {
case 'repeated_messages':
return $this->checkRepeatedMessages($identifier, $context, $config);
case 'rapid_requests':
return $this->checkRapidRequests($identifier, $config);
case 'pattern_spam':
return $this->checkPatternSpam($context['message'] ?? '', $config);
default:
return null;
}
}
private function checkRepeatedMessages(string $identifier, array $context, array $config): ?array
{
$message = $context['message'] ?? '';
$messageHash = md5($message);
$key = "repeated_messages:{$identifier}";
$recent = $this->storage->get($key, []);
$now = time();
// Clean old entries
$recent = array_filter($recent, fn($entry) => $now - $entry['time'] < $config['window']);
// Count repeated messages
$count = count(array_filter($recent, fn($entry) => $entry['hash'] === $messageHash));
if ($count >= $config['limit']) {
return [
'rule' => 'repeated_messages',
'count' => $count,
'limit' => $config['limit'],
];
}
// Store current message
$recent[] = ['hash' => $messageHash, 'time' => $now];
$this->storage->put($key, $recent, $config['window']);
return null;
}
private function calculateBanDuration(int $score): int
{
return match(true) {
$score >= 100 => 86400, // 24 hours
$score >= 70 => 3600, // 1 hour
default => 0
};
}
}Comprehensive logging and monitoring system for security and audit purposes.
class SecurityLogger
{
private $logger;
private bool $encryptLogs;
public function __construct($logger, bool $encryptLogs = false)
{
$this->logger = $logger;
$this->encryptLogs = $encryptLogs;
}
public function logSecurityEvent(string $event, array $context = []): void
{
$logData = [
'event' => $event,
'timestamp' => date('Y-m-d H:i:s'),
'ip' => $context['ip'] ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $context['user_agent'] ?? $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'user_id' => $context['user_id'] ?? null,
'session_id' => $context['session_id'] ?? session_id(),
'data' => $context['data'] ?? [],
];
if ($this->encryptLogs) {
$logData = $this->encryptLogData($logData);
}
$this->logger->warning("Security event: {$event}", $logData);
}
public function logConversation(string $message, string $response, array $context = []): void
{
$this->logSecurityEvent('conversation', [
'data' => [
'message_length' => strlen($message),
'response_length' => strlen($response),
'message_hash' => hash('sha256', $message),
'response_hash' => hash('sha256', $response),
'conversation_id' => $context['conversation_id'] ?? null,
],
'ip' => $context['ip'] ?? null,
'user_id' => $context['user_id'] ?? null,
]);
}
private function encryptLogData(array $data): array
{
// Implement log encryption if needed
return $data;
}
}class SecurityMonitor
{
private $storage;
public function getSecurityMetrics(int $hours = 24): array
{
$since = time() - ($hours * 3600);
return [
'total_requests' => $this->countRequests($since),
'blocked_requests' => $this->countBlockedRequests($since),
'rate_limited' => $this->countRateLimited($since),
'content_violations' => $this->countContentViolations($since),
'top_violators' => $this->getTopViolators($since),
'attack_patterns' => $this->getAttackPatterns($since),
];
}
private function countRequests(int $since): int
{
// Implement request counting
return 0;
}
private function getTopViolators(int $since, int $limit = 10): array
{
// Return top IPs by violation count
return [];
}
}Next: Frontend Integration