Skip to content

Security and Filtering

Rumen Damyanov edited this page Jul 31, 2025 · 1 revision

Security and Filtering

Comprehensive guide to security features, message filtering, and abuse prevention.

Table of Contents

Security Overview

PHP Chatbot includes multiple layers of security to protect against abuse, ensure safe operation, and maintain data privacy.

Security Layers

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 Settings

// 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
    ]
];

Message Filtering

The built-in ChatMessageFilterMiddleware provides configurable content filtering.

Basic Configuration

'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
],

Using the Filter Middleware

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']);

Custom Filter Rules

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;
        }
    }
}

Rate Limiting

Protect against spam and abuse with flexible rate limiting.

Simple 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));
    }
}

Advanced Rate Limiting

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,
        ];
    }
}

Framework Integration

Laravel Rate Limiting

// 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');

Symfony Rate Limiting

# 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 }

Input Validation

Comprehensive input validation prevents malicious data from reaching the AI.

Basic Input Validator

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);
    }
}

Advanced Validation Rules

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;
    }
}

API Key Security

Protect AI provider API keys with encryption and rotation.

Key Encryption

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
    }
}

Key Rotation

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'));
    }
}

Content Filtering

Advanced content filtering for various types of inappropriate content.

Multi-Layer Content Filter

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,
        ];
    }
}

Abuse Prevention

Comprehensive abuse prevention system.

Abuse Detection 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
        };
    }
}

Logging and Monitoring

Comprehensive logging and monitoring system for security and audit purposes.

Security Logger

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;
    }
}

Monitoring Dashboard 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

Clone this wiki locally