Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
454
src/Framework/Waf/Layers/CommandInjectionLayer.php
Normal file
454
src/Framework/Waf/Layers/CommandInjectionLayer.php
Normal file
@@ -0,0 +1,454 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Command Injection Detection Layer
|
||||
*
|
||||
* Detects operating system command injection attempts.
|
||||
* Protects against arbitrary command execution vulnerabilities.
|
||||
*/
|
||||
final class CommandInjectionLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
/** Command injection patterns */
|
||||
private const COMMAND_INJECTION_PATTERNS = [
|
||||
// Command separators
|
||||
'/[;&|`]+/',
|
||||
'/\|\|/',
|
||||
'/&&/',
|
||||
|
||||
// Common Unix/Linux commands
|
||||
'/\b(ls|cat|pwd|whoami|id|uname|ps|netstat|ifconfig|mount)\b/i',
|
||||
'/\b(grep|find|locate|which|whereis|file|head|tail|more|less)\b/i',
|
||||
'/\b(chmod|chown|mkdir|rmdir|rm|cp|mv|ln|touch)\b/i',
|
||||
'/\b(wget|curl|nc|netcat|telnet|ssh|ftp|tftp)\b/i',
|
||||
'/\b(su|sudo|passwd|useradd|userdel|usermod)\b/i',
|
||||
|
||||
// Common Windows commands
|
||||
'/\b(dir|type|copy|del|move|md|rd|cd|cls|ver)\b/i',
|
||||
'/\b(net|ipconfig|ping|tracert|nslookup|arp|route)\b/i',
|
||||
'/\b(tasklist|taskkill|sc|reg|wmic|powershell|cmd)\b/i',
|
||||
'/\b(systeminfo|whoami|echo|set|path)\b/i',
|
||||
|
||||
// System information and process commands
|
||||
'/\b(ps|top|htop|kill|killall|pkill|nohup|jobs|bg|fg)\b/i',
|
||||
'/\b(crontab|at|service|systemctl|chkconfig|update-rc\.d)\b/i',
|
||||
'/\b(iptables|netfilter|firewall|selinux|apparmor)\b/i',
|
||||
|
||||
// File operations and text processing
|
||||
'/\b(awk|sed|sort|uniq|wc|diff|patch|tar|gzip|gunzip)\b/i',
|
||||
'/\b(zip|unzip|compress|uncompress|ar|strings|od|hexdump)\b/i',
|
||||
|
||||
// Network and system utilities
|
||||
'/\b(dig|nslookup|host|whois|traceroute|mtr|tcpdump|wireshark)\b/i',
|
||||
'/\b(lsof|strace|ltrace|gdb|objdump|readelf|nm)\b/i',
|
||||
|
||||
// Dangerous system calls
|
||||
'/\b(exec|system|shell_exec|passthru|eval|popen|proc_open)\b/i',
|
||||
'/\b(Runtime\.getRuntime|ProcessBuilder|cmd\.exe|sh)\b/i',
|
||||
|
||||
// Command substitution
|
||||
'/\$\(.*\)/',
|
||||
'/`[^`]*`/',
|
||||
'/\${.*}/',
|
||||
|
||||
// Input/Output redirection
|
||||
'/[<>]+/',
|
||||
'/\b(tee|xargs)\b/i',
|
||||
|
||||
// Environment variable manipulation
|
||||
'/\$\w+/',
|
||||
'/\bexport\s+\w+=/i',
|
||||
'/\bset\s+\w+=/i',
|
||||
|
||||
// Script interpreters
|
||||
'/\b(bash|sh|csh|tcsh|zsh|fish|ksh|dash)\b/i',
|
||||
'/\b(python|perl|ruby|php|node|java|lua)\b/i',
|
||||
|
||||
// Encoded command attempts
|
||||
'/%5C/', // Backslash
|
||||
'/%7C/', // Pipe
|
||||
'/%26/', // Ampersand
|
||||
'/%3B/', // Semicolon
|
||||
'/%60/', // Backtick
|
||||
|
||||
// Null byte injection for command termination
|
||||
'/\x00/',
|
||||
'/%00/',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(40),
|
||||
confidenceThreshold: Percentage::from(92.0),
|
||||
blockingMode: true,
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 5
|
||||
);
|
||||
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'command_injection';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
try {
|
||||
// Check query parameters
|
||||
foreach ($request->queryParams as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "query parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check POST data
|
||||
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
|
||||
foreach ($request->parsedBody->data as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check headers that commonly contain command injection attempts
|
||||
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie', 'X-Real-IP'];
|
||||
foreach ($suspiciousHeaders as $headerName) {
|
||||
$headerValue = $request->headers->getFirst($headerName);
|
||||
if ($headerValue) {
|
||||
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check request path
|
||||
$detection = $this->analyzeString($request->path, 'request path');
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if (! empty($detections)) {
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
'Command injection attempt detected',
|
||||
LayerStatus::THREAT_DETECTED,
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'No command injection patterns detected',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'Command injection analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze string for command injection patterns
|
||||
*/
|
||||
private function analyzeString(string $input, string $location): ?Detection
|
||||
{
|
||||
$originalInput = $input;
|
||||
|
||||
// Multiple decoding passes
|
||||
$input = urldecode($input);
|
||||
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5);
|
||||
$input = urldecode($input); // Second pass for double encoding
|
||||
|
||||
foreach (self::COMMAND_INJECTION_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $input, $matches)) {
|
||||
|
||||
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '', $input);
|
||||
$riskScore = $this->calculateRiskScore($pattern, $location, $input);
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::COMMAND_INJECTION,
|
||||
severity: $severity,
|
||||
message: "Command injection pattern detected in {$location}",
|
||||
details: [
|
||||
'location' => $location,
|
||||
'pattern' => $pattern,
|
||||
'matched_text' => $matches[0] ?? '',
|
||||
'input_length' => strlen($originalInput),
|
||||
'decoded_input' => substr($input, 0, 200),
|
||||
'original_input' => substr($originalInput, 0, 200),
|
||||
'command_type' => $this->identifyCommandType($pattern, $matches[0] ?? ''),
|
||||
'risk_factors' => $this->analyzeRiskFactors($input),
|
||||
],
|
||||
confidence: 0.88,
|
||||
riskScore: $riskScore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate severity based on pattern and context
|
||||
*/
|
||||
private function calculateSeverity(string $pattern, string $match, string $fullInput): DetectionSeverity
|
||||
{
|
||||
// Critical - System administration commands
|
||||
if (preg_match('/\b(sudo|su|passwd|useradd|rm|chmod|chown)\b/i', $match)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// Critical - Command execution functions
|
||||
if (preg_match('/\b(exec|system|shell_exec|eval)\b/i', $match)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// Critical - Multiple command separators
|
||||
if (preg_match('/[;&|`]{2,}/', $match)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// High - Network commands
|
||||
if (preg_match('/\b(wget|curl|nc|netcat|ssh|ftp)\b/i', $match)) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// High - Command substitution
|
||||
if (preg_match('/(\$\(|\`|`|\${)/', $pattern)) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// High - Process management
|
||||
if (preg_match('/\b(ps|kill|killall|top|htop)\b/i', $match)) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// Medium - File operations
|
||||
if (preg_match('/\b(cat|ls|find|grep|head|tail)\b/i', $match)) {
|
||||
return DetectionSeverity::MEDIUM;
|
||||
}
|
||||
|
||||
// Medium - Single command separators
|
||||
if (preg_match('/[;&|]/', $match)) {
|
||||
return DetectionSeverity::MEDIUM;
|
||||
}
|
||||
|
||||
return DetectionSeverity::LOW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk score
|
||||
*/
|
||||
private function calculateRiskScore(string $pattern, string $location, string $input): float
|
||||
{
|
||||
$baseScore = 75.0;
|
||||
|
||||
// Location-based risk adjustment
|
||||
if ($location === 'request path') {
|
||||
$baseScore += 10.0;
|
||||
} elseif (str_contains($location, 'POST parameter')) {
|
||||
$baseScore += 5.0;
|
||||
}
|
||||
|
||||
// Pattern-based risk adjustment
|
||||
if (preg_match('/\b(sudo|su|rm|passwd)\b/i', $input)) {
|
||||
$baseScore += 20.0;
|
||||
}
|
||||
|
||||
if (preg_match('/[;&|`]{2,}/', $input)) {
|
||||
$baseScore += 15.0;
|
||||
}
|
||||
|
||||
if (preg_match('/(\$\(|\`|`|\${)/', $input)) {
|
||||
$baseScore += 10.0;
|
||||
}
|
||||
|
||||
// Multiple command indicators
|
||||
$commandCount = preg_match_all('/\b(ls|cat|pwd|whoami|wget|curl)\b/i', $input);
|
||||
if ($commandCount > 1) {
|
||||
$baseScore += ($commandCount * 5);
|
||||
}
|
||||
|
||||
return min(100.0, $baseScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify command type for classification
|
||||
*/
|
||||
private function identifyCommandType(string $pattern, string $match): string
|
||||
{
|
||||
if (preg_match('/\b(ls|cat|pwd|find|grep)\b/i', $match)) {
|
||||
return 'file_operations';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(wget|curl|nc|ssh|ftp)\b/i', $match)) {
|
||||
return 'network_operations';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(ps|kill|top|htop|service)\b/i', $match)) {
|
||||
return 'process_management';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(sudo|su|passwd|useradd)\b/i', $match)) {
|
||||
return 'system_administration';
|
||||
}
|
||||
|
||||
if (preg_match('/[;&|`]+/', $pattern)) {
|
||||
return 'command_chaining';
|
||||
}
|
||||
|
||||
if (preg_match('/(\$\(|\`|\${)/', $pattern)) {
|
||||
return 'command_substitution';
|
||||
}
|
||||
|
||||
return 'general_command';
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze additional risk factors
|
||||
*/
|
||||
private function analyzeRiskFactors(string $input): array
|
||||
{
|
||||
$factors = [];
|
||||
|
||||
if (preg_match('/[;&|`]{2,}/', $input)) {
|
||||
$factors[] = 'multiple_command_separators';
|
||||
}
|
||||
|
||||
if (preg_match('/\b(sudo|su)\b/i', $input)) {
|
||||
$factors[] = 'privilege_escalation';
|
||||
}
|
||||
|
||||
if (preg_match('/(\$\w+|%\w+%)/', $input)) {
|
||||
$factors[] = 'environment_variables';
|
||||
}
|
||||
|
||||
if (preg_match('/[<>]+/', $input)) {
|
||||
$factors[] = 'io_redirection';
|
||||
}
|
||||
|
||||
if (preg_match('/%[0-9a-f]{2}/i', $input)) {
|
||||
$factors[] = 'encoded_content';
|
||||
}
|
||||
|
||||
return $factors;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->config->get('priority', 98);
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return Percentage::from($this->config->get('confidence', 0.92) * 100);
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return Duration::fromMilliseconds($this->config->get('timeout', 40));
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// Pre-compile regex patterns if needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [DetectionCategory::COMMAND_INJECTION, DetectionCategory::INJECTION];
|
||||
}
|
||||
}
|
||||
362
src/Framework/Waf/Layers/IntelligentRateLimitLayer.php
Normal file
362
src/Framework/Waf/Layers/IntelligentRateLimitLayer.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\DateTime\Clock;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\RateLimit\RateLimiter;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Intelligent Rate Limiting WAF Layer
|
||||
*
|
||||
* Integrates advanced WAF threat analysis with the existing RateLimit framework.
|
||||
* Uses the enhanced RateLimiter with traffic pattern analysis, burst detection,
|
||||
* and baseline deviation tracking for sophisticated DDoS protection.
|
||||
*/
|
||||
final class IntelligentRateLimitLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
// Rate limiting tiers for different threat levels
|
||||
private const array RATE_LIMITS = [
|
||||
'normal' => ['limit' => 100, 'window' => 60], // 100 req/min
|
||||
'elevated' => ['limit' => 50, 'window' => 60], // 50 req/min
|
||||
'high' => ['limit' => 20, 'window' => 60], // 20 req/min
|
||||
'critical' => ['limit' => 5, 'window' => 60], // 5 req/min
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly RateLimiter $rateLimiter,
|
||||
private readonly Clock $clock
|
||||
) {
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(100),
|
||||
confidenceThreshold: Percentage::from(85.0),
|
||||
blockingMode: true,
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 5,
|
||||
clock: $this->clock
|
||||
);
|
||||
|
||||
$this->metrics = LayerMetrics::empty($this->clock);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'intelligent_rate_limit';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
try {
|
||||
// Extract request context for intelligent analysis
|
||||
$requestContext = $this->buildRequestContext($request);
|
||||
$clientIp = $requestContext['client_ip'];
|
||||
$rateLimitKey = "ip:{$clientIp}";
|
||||
|
||||
// Determine appropriate rate limit based on request characteristics
|
||||
$rateLimitConfig = $this->selectRateLimitTier($requestContext);
|
||||
|
||||
// Perform intelligent rate limit check with threat analysis
|
||||
$rateLimitResult = $this->rateLimiter->checkLimitWithAnalysis(
|
||||
$rateLimitKey,
|
||||
$rateLimitConfig['limit'],
|
||||
$rateLimitConfig['window'],
|
||||
$requestContext
|
||||
);
|
||||
|
||||
// Process results and create detections
|
||||
if (! $rateLimitResult->isAllowed()) {
|
||||
$detections[] = $this->createRateLimitDetection($rateLimitResult, $requestContext);
|
||||
}
|
||||
|
||||
// Check for suspicious patterns even if rate limit not exceeded
|
||||
if ($rateLimitResult->isAttackSuspected()) {
|
||||
$detections[] = $this->createSuspiciousPatternDetection($rateLimitResult, $requestContext);
|
||||
}
|
||||
|
||||
// Check for anomalous traffic patterns
|
||||
if ($rateLimitResult->hasAnomalousTraffic()) {
|
||||
$detections[] = $this->createAnomalyDetection($rateLimitResult, $requestContext);
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
// Determine overall result
|
||||
if (! empty($detections)) {
|
||||
$threatLevel = $rateLimitResult->getThreatLevel();
|
||||
$status = $this->shouldBlock($threatLevel) ? LayerStatus::THREAT_DETECTED : LayerStatus::SUSPICIOUS;
|
||||
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
$this->buildThreatMessage($rateLimitResult, $threatLevel),
|
||||
$status,
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'Traffic patterns within normal parameters',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'Intelligent rate limit analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build comprehensive request context for analysis
|
||||
*/
|
||||
private function buildRequestContext(Request $request): array
|
||||
{
|
||||
return [
|
||||
'client_ip' => $request->server->getRemoteAddr(),
|
||||
'request_path' => $request->path,
|
||||
'user_agent' => $request->headers->getFirst('User-Agent') ?? '',
|
||||
'request_method' => $request->method->value,
|
||||
'query_params' => $request->queryParams,
|
||||
'content_length' => $request->headers->getFirst('Content-Length') ?? '0',
|
||||
'timestamp' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select appropriate rate limit tier based on request characteristics
|
||||
*/
|
||||
private function selectRateLimitTier(array $requestContext): array
|
||||
{
|
||||
$userAgent = strtolower($requestContext['user_agent']);
|
||||
$path = $requestContext['request_path'];
|
||||
|
||||
// Critical paths get stricter limits
|
||||
if (str_contains($path, '/admin/') || str_contains($path, '/api/')) {
|
||||
return self::RATE_LIMITS['elevated'];
|
||||
}
|
||||
|
||||
// Known malicious user agents get strict limits
|
||||
$maliciousPatterns = ['sqlmap', 'nikto', 'scanner', 'bot', 'crawler'];
|
||||
foreach ($maliciousPatterns as $pattern) {
|
||||
if (str_contains($userAgent, $pattern)) {
|
||||
return self::RATE_LIMITS['critical'];
|
||||
}
|
||||
}
|
||||
|
||||
// Resource-intensive operations get elevated limits
|
||||
if (str_contains($path, '/search') || str_contains($path, '/export')) {
|
||||
return self::RATE_LIMITS['elevated'];
|
||||
}
|
||||
|
||||
return self::RATE_LIMITS['normal'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create detection for rate limit exceeded
|
||||
*/
|
||||
private function createRateLimitDetection($rateLimitResult, array $context): Detection
|
||||
{
|
||||
$threatLevel = $rateLimitResult->getThreatLevel();
|
||||
$severity = match ($threatLevel) {
|
||||
'critical' => DetectionSeverity::CRITICAL,
|
||||
'high' => DetectionSeverity::HIGH,
|
||||
'medium' => DetectionSeverity::MEDIUM,
|
||||
default => DetectionSeverity::LOW
|
||||
};
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::RATE_LIMITING,
|
||||
severity: $severity,
|
||||
message: sprintf(
|
||||
'Rate limit exceeded: %d/%d requests from %s (threat level: %s)',
|
||||
$rateLimitResult->getCurrent(),
|
||||
$rateLimitResult->getLimit(),
|
||||
$context['client_ip'],
|
||||
$threatLevel
|
||||
),
|
||||
confidence: Percentage::from(95.0),
|
||||
location: 'request_rate',
|
||||
timestamp: null,
|
||||
context: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create detection for suspicious attack patterns
|
||||
*/
|
||||
private function createSuspiciousPatternDetection($rateLimitResult, array $context): Detection
|
||||
{
|
||||
$attackPatterns = implode(', ', $rateLimitResult->attackPatterns);
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::SUSPICIOUS_BEHAVIOR,
|
||||
severity: DetectionSeverity::HIGH,
|
||||
message: sprintf(
|
||||
'Suspicious attack patterns detected from %s: %s',
|
||||
$context['client_ip'],
|
||||
$attackPatterns
|
||||
),
|
||||
confidence: Percentage::from(85.0),
|
||||
location: 'traffic_analysis',
|
||||
timestamp: null,
|
||||
context: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create detection for traffic anomalies
|
||||
*/
|
||||
private function createAnomalyDetection($rateLimitResult, array $context): Detection
|
||||
{
|
||||
return new Detection(
|
||||
category: DetectionCategory::ANOMALY,
|
||||
severity: DetectionSeverity::MEDIUM,
|
||||
message: sprintf(
|
||||
'Anomalous traffic pattern from %s: %.2f standard deviations from baseline',
|
||||
$context['client_ip'],
|
||||
$rateLimitResult->baselineDeviation ?? 0.0
|
||||
),
|
||||
confidence: Percentage::from(75.0),
|
||||
location: 'baseline_analysis',
|
||||
timestamp: null,
|
||||
context: null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if threat level warrants blocking
|
||||
*/
|
||||
private function shouldBlock(string $threatLevel): bool
|
||||
{
|
||||
return in_array($threatLevel, ['critical', 'high']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build descriptive threat message
|
||||
*/
|
||||
private function buildThreatMessage($rateLimitResult, string $threatLevel): string
|
||||
{
|
||||
$messages = ['Intelligent rate limiting detected threat'];
|
||||
|
||||
if (! $rateLimitResult->isAllowed()) {
|
||||
$messages[] = 'rate limit exceeded';
|
||||
}
|
||||
|
||||
if ($rateLimitResult->isAttackSuspected()) {
|
||||
$messages[] = 'attack patterns identified';
|
||||
}
|
||||
|
||||
if ($rateLimitResult->hasAnomalousTraffic()) {
|
||||
$messages[] = 'traffic anomalies detected';
|
||||
}
|
||||
|
||||
$messages[] = "threat level: {$threatLevel}";
|
||||
|
||||
return implode(', ', $messages);
|
||||
}
|
||||
|
||||
// ===== LayerInterface Implementation =====
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->config->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 200; // High priority for rate limiting
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return $this->config->getEffectiveConfidenceThreshold();
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return $this->config->getEffectiveTimeout();
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = LayerMetrics::empty($this->clock);
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// No warmup needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// No cleanup needed
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [RateLimiter::class, Clock::class];
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [
|
||||
DetectionCategory::RATE_LIMITING,
|
||||
DetectionCategory::SUSPICIOUS_BEHAVIOR,
|
||||
DetectionCategory::ANOMALY,
|
||||
];
|
||||
}
|
||||
}
|
||||
111
src/Framework/Waf/Layers/LayerInterface.php
Normal file
111
src/Framework/Waf/Layers/LayerInterface.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Interface for all WAF security layers
|
||||
* Each layer analyzes requests and returns threat assessment
|
||||
*/
|
||||
interface LayerInterface
|
||||
{
|
||||
/**
|
||||
* Get unique layer name for identification
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Analyze request and return threat assessment
|
||||
*/
|
||||
public function analyze(Request $request): LayerResult;
|
||||
|
||||
/**
|
||||
* Check if layer is enabled and operational
|
||||
*/
|
||||
public function isEnabled(): bool;
|
||||
|
||||
/**
|
||||
* Check layer health status
|
||||
*/
|
||||
public function isHealthy(): bool;
|
||||
|
||||
/**
|
||||
* Get layer priority (higher = processed first)
|
||||
* Allows for dependency-based processing order
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Get layer confidence level in its assessments
|
||||
* Used for weighted threat scoring
|
||||
*/
|
||||
public function getConfidenceLevel(): Percentage;
|
||||
|
||||
/**
|
||||
* Get maximum processing time before timeout
|
||||
*/
|
||||
public function getTimeoutThreshold(): Duration;
|
||||
|
||||
/**
|
||||
* Configure layer at runtime
|
||||
* Allows dynamic reconfiguration without restart
|
||||
*/
|
||||
public function configure(LayerConfig $config): void;
|
||||
|
||||
/**
|
||||
* Get current layer configuration
|
||||
*/
|
||||
public function getConfig(): LayerConfig;
|
||||
|
||||
/**
|
||||
* Get layer performance metrics
|
||||
*/
|
||||
public function getMetrics(): LayerMetrics;
|
||||
|
||||
/**
|
||||
* Reset layer state (for testing/debugging)
|
||||
*/
|
||||
public function reset(): void;
|
||||
|
||||
/**
|
||||
* Warm up layer (preload rules, caches, etc.)
|
||||
* Called during WAF initialization
|
||||
*/
|
||||
public function warmUp(): void;
|
||||
|
||||
/**
|
||||
* Clean up layer resources
|
||||
* Called during WAF shutdown
|
||||
*/
|
||||
public function shutdown(): void;
|
||||
|
||||
/**
|
||||
* Get layer dependencies (other layers this depends on)
|
||||
* Used for proper initialization order
|
||||
*/
|
||||
public function getDependencies(): array;
|
||||
|
||||
/**
|
||||
* Check if layer supports parallel processing
|
||||
*/
|
||||
public function supportsParallelProcessing(): bool;
|
||||
|
||||
/**
|
||||
* Get layer version for compatibility checking
|
||||
*/
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Get supported threat categories
|
||||
* Returns array of DetectionCategory enums this layer can detect
|
||||
*/
|
||||
public function getSupportedCategories(): array;
|
||||
}
|
||||
387
src/Framework/Waf/Layers/PathTraversalLayer.php
Normal file
387
src/Framework/Waf/Layers/PathTraversalLayer.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Path Traversal Detection Layer
|
||||
*
|
||||
* Detects directory traversal and path manipulation attempts.
|
||||
* Protects against unauthorized file access and system disclosure.
|
||||
*/
|
||||
final class PathTraversalLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
/** Path traversal patterns */
|
||||
private const PATH_TRAVERSAL_PATTERNS = [
|
||||
// Classic directory traversal
|
||||
'/\.\.\//',
|
||||
'/\.\.\\\/',
|
||||
'/\.\.\\\\/',
|
||||
|
||||
// Encoded versions
|
||||
'/%2e%2e%2f/',
|
||||
'/%2e%2e%5c/',
|
||||
'/%252e%252e%252f/',
|
||||
'/\.\.\%2f/',
|
||||
'/\.\.\%5c/',
|
||||
|
||||
// Unicode encoded (using hex notation instead of \u)
|
||||
'/\x{002e}\x{002e}\x{002f}/u',
|
||||
'/\x{ff0e}\x{ff0e}\x{ff0f}/u',
|
||||
|
||||
// Alternative representations
|
||||
'/\.\.%252f/',
|
||||
'/\.\.%c0%af/',
|
||||
'/\.\.%c1%9c/',
|
||||
|
||||
// Null byte injection
|
||||
'/\.\.\/.*\x00/',
|
||||
'/\.\.\\\\.*\x00/',
|
||||
|
||||
// System file access attempts
|
||||
'/\/etc\/passwd/',
|
||||
'/\/etc\/shadow/',
|
||||
'/\/etc\/hosts/',
|
||||
'/\/proc\/version/',
|
||||
'/\/proc\/self\/environ/',
|
||||
'/\/windows\/system32\//',
|
||||
'/\/winnt\/system32\//',
|
||||
|
||||
// Common sensitive files
|
||||
'/\/etc\/.*/',
|
||||
'/\/var\/log\/.*/',
|
||||
'/\/root\/.*/',
|
||||
'/\/home\/.*\/\.\w+/',
|
||||
|
||||
// Web application files
|
||||
'/\/web\.config/',
|
||||
'/\/app\.config/',
|
||||
'/\/\.htaccess/',
|
||||
'/\/\.htpasswd/',
|
||||
'/\/wp-config\.php/',
|
||||
'/\/configuration\.php/',
|
||||
'/\/config\.inc\.php/',
|
||||
|
||||
// Backup and temporary files
|
||||
'/.*\.bak$/',
|
||||
'/.*\.backup$/',
|
||||
'/.*\.old$/',
|
||||
'/.*\.tmp$/',
|
||||
'/.*~$/',
|
||||
'/.*\.swp$/',
|
||||
|
||||
// Source code disclosure
|
||||
'/.*\.php\.bak/',
|
||||
'/.*\.asp\.old/',
|
||||
'/.*\.jsp\.tmp/',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(30),
|
||||
confidenceThreshold: Percentage::from(95.0),
|
||||
blockingMode: true,
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 5
|
||||
);
|
||||
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'path_traversal';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
try {
|
||||
// Check query parameters
|
||||
foreach ($request->queryParams as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "query parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check POST data
|
||||
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
|
||||
foreach ($request->parsedBody->data as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check request path (very important for path traversal)
|
||||
$detection = $this->analyzeString($request->path, 'request path');
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
|
||||
// Check specific headers that might contain file paths
|
||||
$pathHeaders = ['Referer', 'X-Original-URL', 'X-Rewrite-URL'];
|
||||
foreach ($pathHeaders as $headerName) {
|
||||
$headerValue = $request->headers->getFirst($headerName);
|
||||
if ($headerValue) {
|
||||
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if (! empty($detections)) {
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
'Path traversal attempt detected',
|
||||
LayerStatus::THREAT_DETECTED,
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'No path traversal patterns detected',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'Path traversal analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze string for path traversal patterns
|
||||
*/
|
||||
private function analyzeString(string $input, string $location): ?Detection
|
||||
{
|
||||
$originalInput = $input;
|
||||
|
||||
// Multiple decoding passes to catch encoded attempts
|
||||
$input = urldecode($input);
|
||||
$input = urldecode($input); // Second pass for double encoding
|
||||
|
||||
foreach (self::PATH_TRAVERSAL_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $input, $matches)) {
|
||||
|
||||
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '');
|
||||
$riskScore = $this->calculateRiskScore($pattern, $location);
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::PATH_TRAVERSAL,
|
||||
severity: $severity,
|
||||
message: "Path traversal pattern detected in {$location}",
|
||||
details: [
|
||||
'location' => $location,
|
||||
'pattern' => $pattern,
|
||||
'matched_text' => $matches[0] ?? '',
|
||||
'input_length' => strlen($originalInput),
|
||||
'decoded_input' => substr($input, 0, 200),
|
||||
'original_input' => substr($originalInput, 0, 200),
|
||||
'threat_type' => $this->identifyThreatType($pattern),
|
||||
],
|
||||
confidence: 0.9,
|
||||
riskScore: $riskScore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate severity based on pattern and context
|
||||
*/
|
||||
private function calculateSeverity(string $pattern, string $match): DetectionSeverity
|
||||
{
|
||||
// Critical - System file access
|
||||
if (preg_match('/\/(etc|proc|root|windows|winnt)\//', $pattern)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// Critical - Sensitive application files
|
||||
if (preg_match('/(passwd|shadow|config|htaccess|htpasswd)/', $pattern)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// High - Directory traversal with sensitive extensions
|
||||
if (preg_match('/\.\.(\/|\\\\).*\.(php|asp|jsp|config)/', $match)) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// High - Multiple directory traversals
|
||||
if (substr_count($match, '..') > 2) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// Medium - Basic directory traversal
|
||||
return DetectionSeverity::MEDIUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk score based on pattern and location
|
||||
*/
|
||||
private function calculateRiskScore(string $pattern, string $location): float
|
||||
{
|
||||
$baseScore = 70.0;
|
||||
|
||||
// Higher risk in request path
|
||||
if ($location === 'request path') {
|
||||
$baseScore += 15.0;
|
||||
}
|
||||
|
||||
// System files are highest risk
|
||||
if (preg_match('/\/(etc|proc|root|windows)\//', $pattern)) {
|
||||
$baseScore += 20.0;
|
||||
}
|
||||
|
||||
// Config files are high risk
|
||||
if (preg_match('/(config|htaccess|passwd)/', $pattern)) {
|
||||
$baseScore += 15.0;
|
||||
}
|
||||
|
||||
// Encoded attempts show deliberate evasion
|
||||
if (preg_match('/%[0-9a-f]{2}/', $pattern)) {
|
||||
$baseScore += 10.0;
|
||||
}
|
||||
|
||||
return min(100.0, $baseScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify the type of threat based on pattern
|
||||
*/
|
||||
private function identifyThreatType(string $pattern): string
|
||||
{
|
||||
if (preg_match('/\/(etc|proc)\//', $pattern)) {
|
||||
return 'system_file_access';
|
||||
}
|
||||
|
||||
if (preg_match('/(config|htaccess|passwd)/', $pattern)) {
|
||||
return 'configuration_disclosure';
|
||||
}
|
||||
|
||||
if (preg_match('/\.(bak|backup|old|tmp)/', $pattern)) {
|
||||
return 'backup_file_access';
|
||||
}
|
||||
|
||||
if (preg_match('/\.\./', $pattern)) {
|
||||
return 'directory_traversal';
|
||||
}
|
||||
|
||||
return 'file_access_attempt';
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->config->get('priority', 95);
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return Percentage::from($this->config->get('confidence', 0.95) * 100);
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return Duration::fromMilliseconds($this->config->get('timeout', 30));
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// Pre-compile regex patterns if needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [DetectionCategory::PATH_TRAVERSAL, DetectionCategory::BROKEN_ACCESS_CONTROL];
|
||||
}
|
||||
}
|
||||
292
src/Framework/Waf/Layers/SqlInjectionLayer.php
Normal file
292
src/Framework/Waf/Layers/SqlInjectionLayer.php
Normal file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* SQL Injection Detection Layer
|
||||
*
|
||||
* Detects SQL injection attempts in request parameters, headers, and body.
|
||||
* Uses pattern matching and heuristic analysis to identify potential SQL injection attacks.
|
||||
*/
|
||||
final class SqlInjectionLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
/** SQL Injection patterns (simplified for initial implementation) */
|
||||
private const SQL_PATTERNS = [
|
||||
// Classic injection patterns
|
||||
'/(\bunion\s+select\b)/i',
|
||||
'/(\bselect\b.*\bfrom\b)/i',
|
||||
'/(\binsert\s+into\b)/i',
|
||||
'/(\bupdate\s+.*\bset\b)/i',
|
||||
'/(\bdelete\s+from\b)/i',
|
||||
'/(\bdrop\s+(table|database)\b)/i',
|
||||
|
||||
// SQL comment indicators
|
||||
'/(--|\#|\/\*|\*\/)/i',
|
||||
|
||||
// SQL operators and functions
|
||||
'/(\bor\s+1\s*=\s*1\b)/i',
|
||||
'/(\band\s+1\s*=\s*1\b)/i',
|
||||
'/(\bor\s+\'[^\']*\'\s*=\s*\'[^\']*\'\b)/i',
|
||||
'/(\bunion\s+all\s+select\b)/i',
|
||||
|
||||
// SQL string manipulation
|
||||
'/(\bconcat\s*\()/i',
|
||||
'/(\bchar\s*\()/i',
|
||||
'/(\bhex\s*\()/i',
|
||||
'/(\bascii\s*\()/i',
|
||||
|
||||
// Database-specific functions
|
||||
'/(\bversion\s*\(\))/i',
|
||||
'/(\buser\s*\(\))/i',
|
||||
'/(\bdatabase\s*\(\))/i',
|
||||
'/(\bsleep\s*\()/i',
|
||||
'/(\bbenchmark\s*\()/i',
|
||||
|
||||
// Blind injection patterns
|
||||
'/(\bwaitfor\s+delay\b)/i',
|
||||
'/(\bif\s*\(.*,.*,.*\))/i',
|
||||
|
||||
// Quote and escape sequences
|
||||
'/([\'\"]\s*;\s*)/i',
|
||||
'/(\\\x[0-9a-f]{2})/i',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(50),
|
||||
confidenceThreshold: Percentage::from(95.0),
|
||||
blockingMode: true,
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 10
|
||||
);
|
||||
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'sql_injection';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
// Debug logging
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Starting analysis for path: " . ($request->path ?? '/') . "\n", FILE_APPEND);
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Query params: " . json_encode($request->queryParams) . "\n", FILE_APPEND);
|
||||
|
||||
try {
|
||||
// Check query parameters
|
||||
foreach ($request->queryParams as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Analyzing query param '{$key}' = '{$value}'\n", FILE_APPEND);
|
||||
$detection = $this->analyzeString($value, "query parameter '{$key}'");
|
||||
if ($detection) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: DETECTION FOUND in query param '{$key}'!\n", FILE_APPEND);
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check POST data
|
||||
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
|
||||
foreach ($request->parsedBody->data as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check headers (common injection points)
|
||||
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie'];
|
||||
foreach ($suspiciousHeaders as $headerName) {
|
||||
$headerValue = $request->headers->getFirst($headerName);
|
||||
if ($headerValue) {
|
||||
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check request path
|
||||
$detection = $this->analyzeString($request->path, 'request path');
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if (! empty($detections)) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Returning THREAT result with " . count($detections) . " detections\n", FILE_APPEND);
|
||||
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
'SQL injection attempt detected',
|
||||
LayerStatus::THREAT_DETECTED,
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'No SQL injection patterns detected',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'SQL injection analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze string for SQL injection patterns
|
||||
*/
|
||||
private function analyzeString(string $input, string $location): ?Detection
|
||||
{
|
||||
$originalInput = $input;
|
||||
$input = urldecode($input); // Decode URL encoding
|
||||
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5); // Decode HTML entities
|
||||
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: analyzeString - original: '{$originalInput}', decoded: '{$input}'\n", FILE_APPEND);
|
||||
|
||||
foreach (self::SQL_PATTERNS as $pattern) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Testing pattern: {$pattern}\n", FILE_APPEND);
|
||||
if (preg_match($pattern, $input, $matches)) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: PATTERN MATCH! Pattern: {$pattern}, Match: " . ($matches[0] ?? '') . "\n", FILE_APPEND);
|
||||
|
||||
try {
|
||||
$detection = new Detection(
|
||||
category: DetectionCategory::SQL_INJECTION,
|
||||
severity: DetectionSeverity::CRITICAL,
|
||||
message: "SQL injection pattern detected in {$location}: " . ($matches[0] ?? ''),
|
||||
ruleId: null,
|
||||
confidence: Percentage::from(90.0),
|
||||
payload: null,
|
||||
location: $location,
|
||||
timestamp: null, // Will be set automatically by static methods if needed
|
||||
context: null
|
||||
);
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: Created Detection object, returning it\n", FILE_APPEND);
|
||||
|
||||
return $detection;
|
||||
} catch (\Throwable $e) {
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: ERROR creating Detection: " . $e->getMessage() . "\n", FILE_APPEND);
|
||||
// Continue to next pattern
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents('/tmp/waf_debug.log', "SqlInjectionLayer: No patterns matched for input: '{$input}'\n", FILE_APPEND);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true; // Simple implementation - always healthy
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100; // High priority for SQL injection detection
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return $this->config->getEffectiveConfidenceThreshold();
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return $this->config->getEffectiveTimeout();
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// Pre-compile regex patterns if needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return []; // No dependencies
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [DetectionCategory::SQL_INJECTION, DetectionCategory::INJECTION];
|
||||
}
|
||||
}
|
||||
541
src/Framework/Waf/Layers/SuspiciousUserAgentLayer.php
Normal file
541
src/Framework/Waf/Layers/SuspiciousUserAgentLayer.php
Normal file
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Suspicious User Agent Detection Layer
|
||||
*
|
||||
* Detects malicious bots, scanners, and suspicious user agents.
|
||||
* Helps identify automated attacks and reconnaissance attempts.
|
||||
*/
|
||||
final class SuspiciousUserAgentLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
/** Suspicious User Agent patterns */
|
||||
private const SUSPICIOUS_PATTERNS = [
|
||||
// Security scanners and vulnerability assessment tools
|
||||
'/nikto/i',
|
||||
'/nessus/i',
|
||||
'/openvas/i',
|
||||
'/nmap/i',
|
||||
'/masscan/i',
|
||||
'/zmap/i',
|
||||
'/sqlmap/i',
|
||||
'/w3af/i',
|
||||
'/owasp/i',
|
||||
'/burp/i',
|
||||
'/dirbuster/i',
|
||||
'/dirb/i',
|
||||
'/gobuster/i',
|
||||
'/ffuf/i',
|
||||
'/wfuzz/i',
|
||||
|
||||
// Web application scanners
|
||||
'/acunetix/i',
|
||||
'/appscan/i',
|
||||
'/webinspect/i',
|
||||
'/netsparker/i',
|
||||
'/qualys/i',
|
||||
'/rapid7/i',
|
||||
'/veracode/i',
|
||||
'/checkmarx/i',
|
||||
|
||||
// Generic scanner indicators
|
||||
'/scanner/i',
|
||||
'/security/i',
|
||||
'/pentest/i',
|
||||
'/audit/i',
|
||||
'/vulnerability/i',
|
||||
'/exploit/i',
|
||||
|
||||
// Command line HTTP clients
|
||||
'/curl/i',
|
||||
'/wget/i',
|
||||
'/http_request/i',
|
||||
'/lwp-request/i',
|
||||
'/python-requests/i',
|
||||
'/python-urllib/i',
|
||||
'/go-http-client/i',
|
||||
'/node-fetch/i',
|
||||
'/axios/i',
|
||||
|
||||
// Scraping and crawling bots (malicious ones)
|
||||
'/scrapy/i',
|
||||
'/beautifulsoup/i',
|
||||
'/mechanize/i',
|
||||
'/selenium/i',
|
||||
'/phantomjs/i',
|
||||
'/headless/i',
|
||||
'/bot/i',
|
||||
'/spider/i',
|
||||
'/crawler/i',
|
||||
|
||||
// SQL injection tools
|
||||
'/havij/i',
|
||||
'/pangolin/i',
|
||||
'/safe3si/i',
|
||||
'/sqlninja/i',
|
||||
'/sqlsus/i',
|
||||
|
||||
// XSS and injection tools
|
||||
'/xsser/i',
|
||||
'/beef/i',
|
||||
'/metasploit/i',
|
||||
'/commix/i',
|
||||
|
||||
// Directory traversal and LFI tools
|
||||
'/fimap/i',
|
||||
'/dotdotpwn/i',
|
||||
'/padbuster/i',
|
||||
|
||||
// Generic attack tools
|
||||
'/hydra/i',
|
||||
'/john/i',
|
||||
'/hashcat/i',
|
||||
'/aircrack/i',
|
||||
'/reaver/i',
|
||||
|
||||
// Suspicious patterns
|
||||
'/\<script/i',
|
||||
'/javascript/i',
|
||||
'/vbscript/i',
|
||||
'/onload/i',
|
||||
'/onerror/i',
|
||||
|
||||
// Empty or very short user agents
|
||||
'/^$/i',
|
||||
'/^.{1,3}$/i',
|
||||
|
||||
// Suspicious single characters or numbers
|
||||
'/^[0-9]{1,2}$/i',
|
||||
'/^[a-z]{1,2}$/i',
|
||||
'/^[\-_\.]{1,3}$/i',
|
||||
|
||||
// Obvious fake user agents
|
||||
'/mozilla\/1\./i',
|
||||
'/mozilla\/2\./i',
|
||||
'/mozilla\/3\./i',
|
||||
'/msie [1-6]\./i',
|
||||
|
||||
// Library and framework indicators in suspicious contexts
|
||||
'/libwww/i',
|
||||
'/winhttp/i',
|
||||
'/java\/1\.[0-4]/i',
|
||||
|
||||
// Reconnaissance tools
|
||||
'/whatweb/i',
|
||||
'/wappalyzer/i',
|
||||
'/builtwith/i',
|
||||
'/httprint/i',
|
||||
'/webtech/i',
|
||||
|
||||
// Load testing tools (potentially abusive)
|
||||
'/apache-httpclient/i',
|
||||
'/jmeter/i',
|
||||
'/siege/i',
|
||||
'/bombardier/i',
|
||||
'/wrk/i',
|
||||
];
|
||||
|
||||
/** Legitimate bot patterns to whitelist */
|
||||
private const LEGITIMATE_BOTS = [
|
||||
'/googlebot/i',
|
||||
'/bingbot/i',
|
||||
'/slurp/i', // Yahoo
|
||||
'/duckduckbot/i',
|
||||
'/baiduspider/i',
|
||||
'/yandexbot/i',
|
||||
'/facebookexternalhit/i',
|
||||
'/twitterbot/i',
|
||||
'/linkedinbot/i',
|
||||
'/whatsapp/i',
|
||||
'/telegrambot/i',
|
||||
'/discordbot/i',
|
||||
'/slackbot/i',
|
||||
'/applebot/i',
|
||||
'/msnbot/i',
|
||||
'/archive\.org_bot/i',
|
||||
'/ia_archiver/i',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(20),
|
||||
confidenceThreshold: Percentage::from(70.0),
|
||||
blockingMode: false, // Usually just flag suspicious agents
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 3
|
||||
);
|
||||
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'suspicious_user_agent';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
try {
|
||||
$userAgent = $request->headers->getFirst('User-Agent', '');
|
||||
|
||||
if (empty($userAgent)) {
|
||||
// Missing User-Agent can be suspicious
|
||||
$detections[] = new Detection(
|
||||
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
|
||||
severity: DetectionSeverity::LOW,
|
||||
message: 'Missing User-Agent header',
|
||||
details: [
|
||||
'user_agent' => '',
|
||||
'reason' => 'missing_header',
|
||||
'client_ip' => $request->server->getClientIp()?->value ?? 'unknown',
|
||||
],
|
||||
confidence: 0.5,
|
||||
riskScore: 30.0
|
||||
);
|
||||
} else {
|
||||
// Check for suspicious patterns
|
||||
$detection = $this->analyzeUserAgent($userAgent);
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if (! empty($detections)) {
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
'Suspicious User-Agent detected',
|
||||
LayerStatus::SUSPICIOUS, // Use SUSPICIOUS instead of THREAT_DETECTED for lower severity
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'User-Agent appears legitimate',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'User-Agent analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze User-Agent string for suspicious patterns
|
||||
*/
|
||||
private function analyzeUserAgent(string $userAgent): ?Detection
|
||||
{
|
||||
// First check if it's a legitimate bot
|
||||
foreach (self::LEGITIMATE_BOTS as $pattern) {
|
||||
if (preg_match($pattern, $userAgent)) {
|
||||
return null; // Legitimate bot, no detection
|
||||
}
|
||||
}
|
||||
|
||||
// Check for suspicious patterns
|
||||
foreach (self::SUSPICIOUS_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $userAgent, $matches)) {
|
||||
|
||||
$threatType = $this->identifyThreatType($pattern, $matches[0] ?? '');
|
||||
$severity = $this->calculateSeverity($threatType, $userAgent);
|
||||
$riskScore = $this->calculateRiskScore($threatType, $userAgent);
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
|
||||
severity: $severity,
|
||||
message: "Suspicious User-Agent detected: {$threatType}",
|
||||
details: [
|
||||
'user_agent' => $userAgent,
|
||||
'matched_pattern' => $pattern,
|
||||
'matched_text' => $matches[0] ?? '',
|
||||
'threat_type' => $threatType,
|
||||
'user_agent_length' => strlen($userAgent),
|
||||
'analysis' => $this->analyzeUserAgentStructure($userAgent),
|
||||
],
|
||||
confidence: $this->calculateConfidence($threatType, $userAgent),
|
||||
riskScore: $riskScore
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Additional heuristic checks
|
||||
return $this->performHeuristicAnalysis($userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify threat type based on pattern
|
||||
*/
|
||||
private function identifyThreatType(string $pattern, string $match): string
|
||||
{
|
||||
if (preg_match('/(nikto|nessus|nmap|sqlmap|burp|scanner|security)/i', $match)) {
|
||||
return 'security_scanner';
|
||||
}
|
||||
|
||||
if (preg_match('/(curl|wget|python|go-http)/i', $match)) {
|
||||
return 'automated_client';
|
||||
}
|
||||
|
||||
if (preg_match('/(scrapy|bot|spider|crawler)/i', $match)) {
|
||||
return 'scraping_bot';
|
||||
}
|
||||
|
||||
if (preg_match('/(script|javascript|vbscript|onload)/i', $match)) {
|
||||
return 'xss_attempt';
|
||||
}
|
||||
|
||||
if (preg_match('/(hydra|john|metasploit)/i', $match)) {
|
||||
return 'attack_tool';
|
||||
}
|
||||
|
||||
if (preg_match('/^.{1,3}$/i', $match)) {
|
||||
return 'suspicious_short';
|
||||
}
|
||||
|
||||
if (preg_match('/(mozilla\/[12]\.|msie [1-6]\.)/i', $match)) {
|
||||
return 'fake_browser';
|
||||
}
|
||||
|
||||
return 'unknown_suspicious';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate severity based on threat type
|
||||
*/
|
||||
private function calculateSeverity(string $threatType, string $userAgent): DetectionSeverity
|
||||
{
|
||||
return match ($threatType) {
|
||||
'security_scanner', 'attack_tool', 'xss_attempt' => DetectionSeverity::HIGH,
|
||||
'automated_client', 'scraping_bot', 'fake_browser' => DetectionSeverity::MEDIUM,
|
||||
'suspicious_short', 'unknown_suspicious' => DetectionSeverity::LOW,
|
||||
default => DetectionSeverity::LOW
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk score
|
||||
*/
|
||||
private function calculateRiskScore(string $threatType, string $userAgent): float
|
||||
{
|
||||
$baseScore = match ($threatType) {
|
||||
'security_scanner' => 85.0,
|
||||
'attack_tool' => 90.0,
|
||||
'xss_attempt' => 80.0,
|
||||
'automated_client' => 60.0,
|
||||
'scraping_bot' => 50.0,
|
||||
'fake_browser' => 45.0,
|
||||
'suspicious_short' => 35.0,
|
||||
'unknown_suspicious' => 40.0,
|
||||
default => 30.0
|
||||
};
|
||||
|
||||
// Adjust based on additional factors
|
||||
if (strlen($userAgent) < 10) {
|
||||
$baseScore += 10.0;
|
||||
}
|
||||
|
||||
if (preg_match('/[<>"\']/', $userAgent)) {
|
||||
$baseScore += 15.0; // HTML/script injection characters
|
||||
}
|
||||
|
||||
if (preg_match('/\b(admin|root|test|hack|exploit)\b/i', $userAgent)) {
|
||||
$baseScore += 10.0;
|
||||
}
|
||||
|
||||
return min(100.0, $baseScore);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate confidence based on threat type and additional factors
|
||||
*/
|
||||
private function calculateConfidence(string $threatType, string $userAgent): float
|
||||
{
|
||||
$baseConfidence = match ($threatType) {
|
||||
'security_scanner', 'attack_tool' => 0.9,
|
||||
'automated_client', 'xss_attempt' => 0.8,
|
||||
'scraping_bot', 'fake_browser' => 0.7,
|
||||
'suspicious_short' => 0.6,
|
||||
'unknown_suspicious' => 0.5,
|
||||
default => 0.4
|
||||
};
|
||||
|
||||
// Adjust confidence based on additional evidence
|
||||
if (preg_match('/[<>"\']/', $userAgent)) {
|
||||
$baseConfidence += 0.1; // HTML/script characters increase confidence
|
||||
}
|
||||
|
||||
if (strlen($userAgent) < 5) {
|
||||
$baseConfidence += 0.1; // Very short user agents are more suspicious
|
||||
}
|
||||
|
||||
return min(1.0, $baseConfidence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform heuristic analysis for patterns not caught by regex
|
||||
*/
|
||||
private function performHeuristicAnalysis(string $userAgent): ?Detection
|
||||
{
|
||||
// Check for extremely long user agents (potential buffer overflow attempts)
|
||||
if (strlen($userAgent) > 2000) {
|
||||
return new Detection(
|
||||
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
|
||||
severity: DetectionSeverity::MEDIUM,
|
||||
message: 'Extremely long User-Agent detected',
|
||||
details: [
|
||||
'user_agent' => substr($userAgent, 0, 200) . '...',
|
||||
'user_agent_length' => strlen($userAgent),
|
||||
'threat_type' => 'oversized_header',
|
||||
'reason' => 'potential_overflow_attempt',
|
||||
],
|
||||
confidence: 0.7,
|
||||
riskScore: 65.0
|
||||
);
|
||||
}
|
||||
|
||||
// Check for repeated suspicious characters
|
||||
if (preg_match('/([<>"\'])\1{5,}/', $userAgent)) {
|
||||
return new Detection(
|
||||
category: DetectionCategory::SUSPICIOUS_USER_AGENT,
|
||||
severity: DetectionSeverity::MEDIUM,
|
||||
message: 'User-Agent contains repeated suspicious characters',
|
||||
details: [
|
||||
'user_agent' => $userAgent,
|
||||
'threat_type' => 'pattern_anomaly',
|
||||
'reason' => 'repeated_special_characters',
|
||||
],
|
||||
confidence: 0.6,
|
||||
riskScore: 55.0
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze User-Agent structure for additional insights
|
||||
*/
|
||||
private function analyzeUserAgentStructure(string $userAgent): array
|
||||
{
|
||||
return [
|
||||
'length' => strlen($userAgent),
|
||||
'has_mozilla' => str_contains(strtolower($userAgent), 'mozilla'),
|
||||
'has_webkit' => str_contains(strtolower($userAgent), 'webkit'),
|
||||
'has_version' => preg_match('/\d+\.\d+/', $userAgent) ? true : false,
|
||||
'special_chars' => preg_match_all('/[<>"\'\(\)\[\]{}]/', $userAgent),
|
||||
'suspicious_keywords' => preg_match_all('/\b(hack|exploit|attack|scan|test|admin|root)\b/i', $userAgent),
|
||||
];
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->config->get('priority', 20);
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return Percentage::from($this->config->get('confidence', 0.7) * 100);
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return Duration::fromMilliseconds($this->config->get('timeout', 20));
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// Pre-compile regex patterns if needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [
|
||||
DetectionCategory::SUSPICIOUS_USER_AGENT,
|
||||
DetectionCategory::MALICIOUS_BOT,
|
||||
DetectionCategory::RECONNAISSANCE,
|
||||
];
|
||||
}
|
||||
}
|
||||
341
src/Framework/Waf/Layers/XssLayer.php
Normal file
341
src/Framework/Waf/Layers/XssLayer.php
Normal file
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Waf\Layers;
|
||||
|
||||
use App\Framework\Core\ValueObjects\Duration;
|
||||
use App\Framework\Core\ValueObjects\Percentage;
|
||||
use App\Framework\Http\Request;
|
||||
use App\Framework\Waf\DetectionCategory;
|
||||
use App\Framework\Waf\DetectionSeverity;
|
||||
use App\Framework\Waf\LayerResult;
|
||||
use App\Framework\Waf\LayerStatus;
|
||||
use App\Framework\Waf\ValueObjects\Detection;
|
||||
use App\Framework\Waf\ValueObjects\LayerConfig;
|
||||
use App\Framework\Waf\ValueObjects\LayerMetrics;
|
||||
|
||||
/**
|
||||
* Cross-Site Scripting (XSS) Detection Layer
|
||||
*
|
||||
* Detects XSS attempts in request parameters, headers, and body.
|
||||
* Covers reflected, stored, and DOM-based XSS patterns.
|
||||
*/
|
||||
final class XssLayer implements LayerInterface
|
||||
{
|
||||
private LayerConfig $config;
|
||||
|
||||
private LayerMetrics $metrics;
|
||||
|
||||
private bool $enabled = true;
|
||||
|
||||
/** XSS patterns (simplified for initial implementation) */
|
||||
private const XSS_PATTERNS = [
|
||||
// Script tags
|
||||
'/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/i',
|
||||
'/<script\b/i',
|
||||
'/<\/script>/i',
|
||||
|
||||
// Event handlers
|
||||
'/\bon\w+\s*=/i',
|
||||
'/\bonload\s*=/i',
|
||||
'/\bonclick\s*=/i',
|
||||
'/\bonerror\s*=/i',
|
||||
'/\bonmouseover\s*=/i',
|
||||
'/\bonfocus\s*=/i',
|
||||
|
||||
// JavaScript protocols
|
||||
'/javascript\s*:/i',
|
||||
'/vbscript\s*:/i',
|
||||
'/data\s*:/i',
|
||||
|
||||
// HTML entities for script injection
|
||||
'/<script/i',
|
||||
'/><\/script>/i',
|
||||
'/<script/i',
|
||||
'/<script/i',
|
||||
|
||||
// Common XSS vectors
|
||||
'/<iframe\b/i',
|
||||
'/<object\b/i',
|
||||
'/<embed\b/i',
|
||||
'/<applet\b/i',
|
||||
'/<meta\b/i',
|
||||
'/<link\b/i',
|
||||
'/<style\b/i',
|
||||
|
||||
// JavaScript functions
|
||||
'/alert\s*\(/i',
|
||||
'/confirm\s*\(/i',
|
||||
'/prompt\s*\(/i',
|
||||
'/eval\s*\(/i',
|
||||
'/setTimeout\s*\(/i',
|
||||
'/setInterval\s*\(/i',
|
||||
'/document\.(write|writeln)/i',
|
||||
'/window\.(location|open)/i',
|
||||
|
||||
// Encoded XSS attempts
|
||||
'/%3Cscript/i',
|
||||
'/%3C%2Fscript%3E/i',
|
||||
'/\\\x3cscript/i',
|
||||
'/\\\u003cscript/i',
|
||||
|
||||
// Expression() CSS injection
|
||||
'/expression\s*\(/i',
|
||||
'/javascript\s*\:/i',
|
||||
'/-moz-binding/i',
|
||||
|
||||
// SVG XSS vectors
|
||||
'/<svg\b/i',
|
||||
'/onload\s*=/i',
|
||||
'/onclick\s*=/i',
|
||||
|
||||
// Base64 encoded attempts (common patterns)
|
||||
'/PHNjcmlwdA==/i', // <script base64
|
||||
'/YWxlcnQ=/i', // alert base64
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->config = new LayerConfig(
|
||||
enabled: true,
|
||||
timeout: Duration::fromMilliseconds(50),
|
||||
confidenceThreshold: Percentage::from(90.0),
|
||||
blockingMode: true,
|
||||
logDetections: true,
|
||||
maxDetectionsPerRequest: 10
|
||||
);
|
||||
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'xss';
|
||||
}
|
||||
|
||||
public function analyze(Request $request): LayerResult
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$detections = [];
|
||||
|
||||
try {
|
||||
// Check query parameters
|
||||
foreach ($request->queryParams as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "query parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check POST data
|
||||
if (isset($request->parsedBody->data) && is_array($request->parsedBody->data)) {
|
||||
foreach ($request->parsedBody->data as $key => $value) {
|
||||
if (is_string($value)) {
|
||||
$detection = $this->analyzeString($value, "POST parameter '{$key}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check headers (especially User-Agent and Referer)
|
||||
$suspiciousHeaders = ['User-Agent', 'Referer', 'X-Forwarded-For', 'Cookie'];
|
||||
foreach ($suspiciousHeaders as $headerName) {
|
||||
$headerValue = $request->headers->getFirst($headerName);
|
||||
if ($headerValue) {
|
||||
$detection = $this->analyzeString($headerValue, "header '{$headerName}'");
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check request path
|
||||
$detection = $this->analyzeString($request->path, 'request path');
|
||||
if ($detection) {
|
||||
$detections[] = $detection;
|
||||
}
|
||||
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
if (! empty($detections)) {
|
||||
return LayerResult::threat(
|
||||
$this->getName(),
|
||||
'XSS attempt detected',
|
||||
LayerStatus::THREAT_DETECTED,
|
||||
$detections,
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
|
||||
return LayerResult::clean(
|
||||
$this->getName(),
|
||||
'No XSS patterns detected',
|
||||
$processingTime
|
||||
);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$processingTime = Duration::fromMilliseconds((microtime(true) - $startTime) * 1000);
|
||||
|
||||
return LayerResult::error(
|
||||
$this->getName(),
|
||||
'XSS analysis failed: ' . $e->getMessage(),
|
||||
$processingTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze string for XSS patterns
|
||||
*/
|
||||
private function analyzeString(string $input, string $location): ?Detection
|
||||
{
|
||||
$originalInput = $input;
|
||||
|
||||
// Multiple decoding passes to catch double-encoded attempts
|
||||
$input = urldecode($input);
|
||||
$input = html_entity_decode($input, ENT_QUOTES | ENT_HTML5);
|
||||
$input = urldecode($input); // Second pass for double encoding
|
||||
|
||||
foreach (self::XSS_PATTERNS as $pattern) {
|
||||
if (preg_match($pattern, $input, $matches)) {
|
||||
|
||||
// Calculate severity based on pattern matched
|
||||
$severity = $this->calculateSeverity($pattern, $matches[0] ?? '');
|
||||
|
||||
return new Detection(
|
||||
category: DetectionCategory::XSS,
|
||||
severity: $severity,
|
||||
message: "XSS pattern detected in {$location}",
|
||||
details: [
|
||||
'location' => $location,
|
||||
'pattern' => $pattern,
|
||||
'matched_text' => $matches[0] ?? '',
|
||||
'input_length' => strlen($originalInput),
|
||||
'decoded_input' => substr($input, 0, 200), // Limit for logging
|
||||
'original_input' => substr($originalInput, 0, 200),
|
||||
],
|
||||
confidence: 0.85,
|
||||
riskScore: $this->calculateRiskScore($pattern)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate severity based on XSS pattern
|
||||
*/
|
||||
private function calculateSeverity(string $pattern, string $match): DetectionSeverity
|
||||
{
|
||||
// Critical patterns
|
||||
if (preg_match('/(script|eval|setTimeout|setInterval)/i', $pattern)) {
|
||||
return DetectionSeverity::CRITICAL;
|
||||
}
|
||||
|
||||
// High severity patterns
|
||||
if (preg_match('/(javascript|vbscript|on\w+|alert|confirm)/i', $pattern)) {
|
||||
return DetectionSeverity::HIGH;
|
||||
}
|
||||
|
||||
// Medium severity for other patterns
|
||||
return DetectionSeverity::MEDIUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate risk score based on pattern
|
||||
*/
|
||||
private function calculateRiskScore(string $pattern): float
|
||||
{
|
||||
// Critical patterns get higher scores
|
||||
if (preg_match('/(script|eval|setTimeout|setInterval)/i', $pattern)) {
|
||||
return 90.0;
|
||||
}
|
||||
|
||||
if (preg_match('/(javascript|vbscript|on\w+)/i', $pattern)) {
|
||||
return 80.0;
|
||||
}
|
||||
|
||||
return 70.0;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return $this->enabled;
|
||||
}
|
||||
|
||||
public function isHealthy(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->config->get('priority', 90);
|
||||
}
|
||||
|
||||
public function getConfidenceLevel(): Percentage
|
||||
{
|
||||
return Percentage::from($this->config->get('confidence', 0.9) * 100);
|
||||
}
|
||||
|
||||
public function getTimeoutThreshold(): Duration
|
||||
{
|
||||
return Duration::fromMilliseconds($this->config->get('timeout', 50));
|
||||
}
|
||||
|
||||
public function configure(LayerConfig $config): void
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getConfig(): LayerConfig
|
||||
{
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
public function getMetrics(): LayerMetrics
|
||||
{
|
||||
return $this->metrics;
|
||||
}
|
||||
|
||||
public function reset(): void
|
||||
{
|
||||
$this->metrics = new LayerMetrics();
|
||||
}
|
||||
|
||||
public function warmUp(): void
|
||||
{
|
||||
// Pre-compile regex patterns if needed
|
||||
}
|
||||
|
||||
public function shutdown(): void
|
||||
{
|
||||
// Cleanup resources
|
||||
}
|
||||
|
||||
public function getDependencies(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function supportsParallelProcessing(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getVersion(): string
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
public function getSupportedCategories(): array
|
||||
{
|
||||
return [DetectionCategory::XSS, DetectionCategory::INJECTION];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user