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:
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\Security;
|
||||
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
/**
|
||||
* Ausnahme für Path-Traversal-Angriffs-Versuche
|
||||
*
|
||||
* Verwendet OWASP-konforme Nachrichten für Path-Traversal-Detection
|
||||
*/
|
||||
final class PathTraversalAttemptException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* @param string $clientIp Client-IP für Security-Tracking
|
||||
* @param string $requestedPath Angeforderte Pfad
|
||||
* @param string $pattern Erkanntes Path-Traversal-Pattern
|
||||
* @param string $attackContext Kontext (file_access, url_parameter, etc.)
|
||||
* @param \Throwable|null $previous Vorherige Ausnahme
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $clientIp,
|
||||
public readonly string $requestedPath,
|
||||
public readonly string $pattern,
|
||||
public readonly string $attackContext = 'file_access',
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
// OWASP-konforme Nachricht mit Platzhaltern
|
||||
$message = "Client {$this->clientIp} attempted path traversal: {$this->requestedPath}";
|
||||
|
||||
$context = ExceptionContext::forOperation('security.path_traversal_detection', 'Security')
|
||||
->withData([
|
||||
'client_ip' => $this->clientIp,
|
||||
'requested_path' => $this->requestedPath,
|
||||
'pattern' => $this->pattern,
|
||||
'context' => $this->attackContext,
|
||||
'path_depth' => $this->calculateTraversalDepth(),
|
||||
'event_identifier' => "security_path_traversal:{$this->clientIp}",
|
||||
'category' => 'file_access',
|
||||
'requires_alert' => true, // Path-Traversal-Versuche erfordern immer Alerts
|
||||
])
|
||||
->withMetadata([
|
||||
'security_event' => true,
|
||||
'owasp_compliant' => true,
|
||||
'log_level' => 'ERROR',
|
||||
'attack_type' => 'path_traversal',
|
||||
'critical_security_event' => true,
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: 400, // Bad Request
|
||||
previous: $previous,
|
||||
errorCode: ErrorCode::SECURITY_PATH_TRAVERSAL
|
||||
);
|
||||
}
|
||||
|
||||
// === Factory Methods für verschiedene Path-Traversal-Patterns ===
|
||||
|
||||
public static function dotDotSlash(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, '../ pattern', 'file_access');
|
||||
}
|
||||
|
||||
public static function dotDotBackslash(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, '..\ pattern', 'file_access');
|
||||
}
|
||||
|
||||
public static function encodedTraversal(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, 'URL-encoded traversal', 'file_access');
|
||||
}
|
||||
|
||||
public static function unicodeTraversal(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, 'Unicode-encoded traversal', 'file_access');
|
||||
}
|
||||
|
||||
public static function absolutePath(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, 'Absolute path access', 'file_access');
|
||||
}
|
||||
|
||||
public static function systemPath(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, 'System directory access', 'system_access');
|
||||
}
|
||||
|
||||
public static function configFileAccess(string $clientIp, string $path): self
|
||||
{
|
||||
return new self($clientIp, $path, 'Configuration file access', 'config_access');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt OWASP-konforme Event-Daten zurück
|
||||
*/
|
||||
public function getSecurityEventData(): array
|
||||
{
|
||||
return [
|
||||
'event_identifier' => "security_path_traversal:{$this->clientIp}",
|
||||
'description' => "Client {$this->clientIp} attempted path traversal: {$this->requestedPath}",
|
||||
'category' => 'file_access',
|
||||
'log_level' => 'ERROR',
|
||||
'requires_alert' => true,
|
||||
'client_ip' => $this->clientIp,
|
||||
'requested_path' => $this->requestedPath,
|
||||
'pattern' => $this->pattern,
|
||||
'context' => $this->attackContext,
|
||||
'attack_severity' => $this->getAttackSeverity(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt Schweregrad des Path-Traversal-Angriffs
|
||||
*/
|
||||
public function getAttackSeverity(): string
|
||||
{
|
||||
// Systemverzeichnisse sind kritisch
|
||||
$criticalPaths = ['/etc/', '/var/', '/usr/', '/sys/', '/proc/', 'C:\Windows', 'C:\System'];
|
||||
foreach ($criticalPaths as $critical) {
|
||||
if (str_contains($this->requestedPath, $critical)) {
|
||||
return 'CRITICAL';
|
||||
}
|
||||
}
|
||||
|
||||
// Konfigurationsdateien sind hochriskant
|
||||
$sensitiveFiles = ['.env', 'config', 'passwd', 'shadow', 'hosts', 'web.config'];
|
||||
foreach ($sensitiveFiles as $sensitive) {
|
||||
if (str_contains($this->requestedPath, $sensitive)) {
|
||||
return 'HIGH';
|
||||
}
|
||||
}
|
||||
|
||||
// Deep traversal ist verdächtig
|
||||
if ($this->calculateTraversalDepth() > 3) {
|
||||
return 'HIGH';
|
||||
}
|
||||
|
||||
return 'MEDIUM';
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Tiefe des Traversal-Versuchs
|
||||
*/
|
||||
public function calculateTraversalDepth(): int
|
||||
{
|
||||
return substr_count($this->requestedPath, '../') + substr_count($this->requestedPath, '..\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Path-Traversal-Versuche erfordern immer Alerts
|
||||
*/
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob es ein automatisierter Angriff ist
|
||||
*/
|
||||
public function isAutomatedAttack(): bool
|
||||
{
|
||||
// Typische automatisierte Scanner-Patterns
|
||||
$automatedPatterns = [
|
||||
'../../../../../../../../etc/passwd',
|
||||
'..\\..\\..\\..\\windows\\system32',
|
||||
'%2e%2e%2f', // URL-encoded ../
|
||||
'\x2e\x2e\x2f', // Hex-encoded
|
||||
];
|
||||
|
||||
$lowerPath = strtolower($this->requestedPath);
|
||||
foreach ($automatedPatterns as $pattern) {
|
||||
if (str_contains($lowerPath, strtolower($pattern))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert das Path-Traversal-Pattern detailliert
|
||||
*/
|
||||
public function analyzePattern(): array
|
||||
{
|
||||
$analysis = [
|
||||
'encoding_type' => 'none',
|
||||
'target_os' => 'unknown',
|
||||
'target_files' => [],
|
||||
'traversal_depth' => $this->calculateTraversalDepth(),
|
||||
'automation_detected' => $this->isAutomatedAttack(),
|
||||
'evasion_techniques' => [],
|
||||
];
|
||||
|
||||
// Encoding-Erkennung
|
||||
if (str_contains($this->requestedPath, '%')) {
|
||||
$analysis['encoding_type'] = 'url_encoded';
|
||||
} elseif (str_contains($this->requestedPath, '\x')) {
|
||||
$analysis['encoding_type'] = 'hex_encoded';
|
||||
} elseif (preg_match('/\\u[0-9a-f]{4}/i', $this->requestedPath)) {
|
||||
$analysis['encoding_type'] = 'unicode_encoded';
|
||||
}
|
||||
|
||||
// OS-Erkennung
|
||||
if (str_contains($this->requestedPath, '\\')) {
|
||||
$analysis['target_os'] = 'windows';
|
||||
} elseif (str_contains($this->requestedPath, '/')) {
|
||||
$analysis['target_os'] = 'unix';
|
||||
}
|
||||
|
||||
// Zieldateien identifizieren
|
||||
$targetFiles = [
|
||||
'passwd' => 'unix_password_file',
|
||||
'shadow' => 'unix_shadow_file',
|
||||
'hosts' => 'hosts_file',
|
||||
'.env' => 'environment_file',
|
||||
'config' => 'configuration_file',
|
||||
'web.config' => 'iis_config',
|
||||
'httpd.conf' => 'apache_config',
|
||||
'nginx.conf' => 'nginx_config',
|
||||
];
|
||||
|
||||
foreach ($targetFiles as $file => $description) {
|
||||
if (str_contains(strtolower($this->requestedPath), $file)) {
|
||||
$analysis['target_files'][] = $description;
|
||||
}
|
||||
}
|
||||
|
||||
// Evasion-Techniken
|
||||
if (str_contains($this->requestedPath, './')) {
|
||||
$analysis['evasion_techniques'][] = 'current_directory_reference';
|
||||
}
|
||||
|
||||
if (preg_match('/\.{3,}/', $this->requestedPath)) {
|
||||
$analysis['evasion_techniques'][] = 'multiple_dots';
|
||||
}
|
||||
|
||||
if (str_contains($this->requestedPath, '//')) {
|
||||
$analysis['evasion_techniques'][] = 'double_slash';
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt benutzerfreundliche Fehlermeldung zurück (ohne Details zu verraten)
|
||||
*/
|
||||
public function getUserMessage(): string
|
||||
{
|
||||
return "Invalid file path. Access denied.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt spezifische Verteidigungsempfehlung zurück
|
||||
*/
|
||||
public function getDefenseRecommendation(): string
|
||||
{
|
||||
return match ($this->attackContext) {
|
||||
'file_access' => 'Implement proper path validation, use allowlists, and restrict file access to specific directories.',
|
||||
'system_access' => 'Block access to system directories, implement strict path validation, and use security contexts.',
|
||||
'config_access' => 'Secure configuration files, implement access controls, and use environment variables.',
|
||||
default => 'Implement comprehensive path validation and access controls.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert IOC (Indicator of Compromise) für Security-Teams
|
||||
*/
|
||||
public function generateIOC(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'path_traversal_attempt',
|
||||
'source_ip' => $this->clientIp,
|
||||
'requested_path' => $this->requestedPath,
|
||||
'pattern' => $this->pattern,
|
||||
'context' => $this->attackContext,
|
||||
'severity' => $this->getAttackSeverity(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'automated' => $this->isAutomatedAttack(),
|
||||
'analysis' => $this->analyzePattern(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sofortige Gegenmaßnahmen erforderlich sind
|
||||
*/
|
||||
public function requiresImmediateAction(): bool
|
||||
{
|
||||
return in_array($this->getAttackSeverity(), ['HIGH', 'CRITICAL']) ||
|
||||
$this->isAutomatedAttack() ||
|
||||
$this->calculateTraversalDepth() > 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt WAF-Regel-Vorschläge zurück
|
||||
*/
|
||||
public function getWafRuleSuggestions(): array
|
||||
{
|
||||
$rules = [
|
||||
'Block requests containing ../ or ..\\ patterns',
|
||||
'Block requests with URL-encoded path traversal sequences',
|
||||
'Block access to sensitive file extensions (.env, .config, etc.)',
|
||||
];
|
||||
|
||||
$analysis = $this->analyzePattern();
|
||||
|
||||
if ($analysis['encoding_type'] !== 'none') {
|
||||
$rules[] = 'Implement URL decoding before path traversal detection';
|
||||
}
|
||||
|
||||
if (! empty($analysis['target_files'])) {
|
||||
$rules[] = 'Block access to system configuration files and password files';
|
||||
}
|
||||
|
||||
if ($analysis['traversal_depth'] > 3) {
|
||||
$rules[] = 'Limit maximum directory traversal depth';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt sichere Alternative für File-Access vor
|
||||
*/
|
||||
public function getSecureAlternatives(): array
|
||||
{
|
||||
return [
|
||||
'Use file IDs instead of file paths in URLs',
|
||||
'Implement a file mapping table',
|
||||
'Restrict file access to a specific directory',
|
||||
'Use symbolic links instead of direct paths',
|
||||
'Implement role-based file access controls',
|
||||
'Validate file paths against an allowlist',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\Security;
|
||||
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
/**
|
||||
* Ausnahme für SQL-Injection-Versuche
|
||||
*
|
||||
* Verwendet OWASP-konforme Nachrichten für SQL-Injection-Detection
|
||||
*/
|
||||
final class SqlInjectionAttemptException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* @param string $clientIp Client-IP für Security-Tracking
|
||||
* @param string $field Betroffenes Feld/Parameter
|
||||
* @param string $pattern Erkanntes SQL-Injection-Pattern
|
||||
* @param string $originalValue Ursprünglicher Wert (sanitized)
|
||||
* @param string $detectionMethod Erkennungsmethode (regex, heuristic, etc.)
|
||||
* @param \Throwable|null $previous Vorherige Ausnahme
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $clientIp,
|
||||
public readonly string $field,
|
||||
public readonly string $pattern,
|
||||
public readonly string $originalValue,
|
||||
public readonly string $detectionMethod = 'pattern_match',
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
// OWASP-konforme Nachricht mit Platzhaltern
|
||||
$message = "Client {$this->clientIp} attempted SQL injection in field {$this->field}";
|
||||
|
||||
$context = ExceptionContext::forOperation('security.sql_injection_detection', 'Security')
|
||||
->withData([
|
||||
'client_ip' => $this->clientIp,
|
||||
'field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'detection_method' => $this->detectionMethod,
|
||||
'value_length' => strlen($this->originalValue),
|
||||
'event_identifier' => "security_sql_injection:{$this->clientIp},{$this->field}",
|
||||
'category' => 'input_validation',
|
||||
'requires_alert' => true, // SQL-Injection-Versuche erfordern immer Alerts
|
||||
])
|
||||
->withDebug([
|
||||
'sanitized_value' => $this->sanitizeValueForLog($this->originalValue),
|
||||
])
|
||||
->withMetadata([
|
||||
'security_event' => true,
|
||||
'owasp_compliant' => true,
|
||||
'log_level' => 'ERROR',
|
||||
'attack_type' => 'sql_injection',
|
||||
'critical_security_event' => true,
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: 400, // Bad Request
|
||||
previous: $previous,
|
||||
errorCode: ErrorCode::SECURITY_SQL_INJECTION
|
||||
);
|
||||
}
|
||||
|
||||
// === Factory Methods für verschiedene SQL-Injection-Patterns ===
|
||||
|
||||
public static function unionSelect(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'UNION SELECT', $value, 'union_detection');
|
||||
}
|
||||
|
||||
public static function commentInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'SQL Comment (-- or /*)', $value, 'comment_detection');
|
||||
}
|
||||
|
||||
public static function blindSqlInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Blind SQL Injection', $value, 'blind_detection');
|
||||
}
|
||||
|
||||
public static function timeBasedInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Time-based SQL Injection', $value, 'time_based_detection');
|
||||
}
|
||||
|
||||
public static function errorBasedInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Error-based SQL Injection', $value, 'error_based_detection');
|
||||
}
|
||||
|
||||
public static function booleanBasedInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Boolean-based SQL Injection', $value, 'boolean_detection');
|
||||
}
|
||||
|
||||
public static function stackedQueries(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Stacked Queries', $value, 'stacked_queries_detection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt OWASP-konforme Event-Daten zurück
|
||||
*/
|
||||
public function getSecurityEventData(): array
|
||||
{
|
||||
return [
|
||||
'event_identifier' => "security_sql_injection:{$this->clientIp},{$this->field}",
|
||||
'description' => "Client {$this->clientIp} attempted SQL injection in field {$this->field}",
|
||||
'category' => 'input_validation',
|
||||
'log_level' => 'ERROR',
|
||||
'requires_alert' => true,
|
||||
'client_ip' => $this->clientIp,
|
||||
'field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'detection_method' => $this->detectionMethod,
|
||||
'attack_severity' => $this->getAttackSeverity(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt Schweregrad des Angriffs
|
||||
*/
|
||||
public function getAttackSeverity(): string
|
||||
{
|
||||
return match ($this->detectionMethod) {
|
||||
'union_detection' => 'HIGH',
|
||||
'stacked_queries_detection' => 'CRITICAL',
|
||||
'error_based_detection' => 'HIGH',
|
||||
'time_based_detection' => 'MEDIUM',
|
||||
'blind_detection' => 'MEDIUM',
|
||||
'boolean_detection' => 'MEDIUM',
|
||||
'comment_detection' => 'LOW',
|
||||
default => 'MEDIUM',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL-Injection-Versuche erfordern immer Alerts
|
||||
*/
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob es ein automatisierter Angriff ist
|
||||
*/
|
||||
public function isAutomatedAttack(): bool
|
||||
{
|
||||
// Heuristiken für automatisierte Angriffe
|
||||
$automatedPatterns = [
|
||||
'sqlmap',
|
||||
'havij',
|
||||
'pangolin',
|
||||
'union select',
|
||||
'order by',
|
||||
'group by',
|
||||
];
|
||||
|
||||
$lowerValue = strtolower($this->originalValue);
|
||||
foreach ($automatedPatterns as $pattern) {
|
||||
if (str_contains($lowerValue, $pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert das SQL-Injection-Pattern
|
||||
*/
|
||||
public function analyzePattern(): array
|
||||
{
|
||||
$analysis = [
|
||||
'type' => 'unknown',
|
||||
'technique' => 'unknown',
|
||||
'payload_type' => 'unknown',
|
||||
'database_fingerprint' => 'unknown',
|
||||
'automation_detected' => $this->isAutomatedAttack(),
|
||||
];
|
||||
|
||||
$lowerValue = strtolower($this->originalValue);
|
||||
|
||||
// Typ-Erkennung
|
||||
if (str_contains($lowerValue, 'union')) {
|
||||
$analysis['type'] = 'union_based';
|
||||
} elseif (str_contains($lowerValue, 'sleep(') || str_contains($lowerValue, 'waitfor')) {
|
||||
$analysis['type'] = 'time_based';
|
||||
} elseif (str_contains($lowerValue, '1=1') || str_contains($lowerValue, '1=0')) {
|
||||
$analysis['type'] = 'boolean_based';
|
||||
} elseif (str_contains($lowerValue, 'error') || str_contains($lowerValue, 'convert(')) {
|
||||
$analysis['type'] = 'error_based';
|
||||
}
|
||||
|
||||
// Technik-Erkennung
|
||||
if (str_contains($lowerValue, '/*') || str_contains($lowerValue, '--')) {
|
||||
$analysis['technique'] = 'comment_bypassing';
|
||||
} elseif (str_contains($lowerValue, 'char(') || str_contains($lowerValue, 'ascii(')) {
|
||||
$analysis['technique'] = 'encoding_bypassing';
|
||||
} elseif (str_contains($lowerValue, ';')) {
|
||||
$analysis['technique'] = 'stacked_queries';
|
||||
}
|
||||
|
||||
// Database-Fingerprinting
|
||||
if (str_contains($lowerValue, 'mysql') || str_contains($lowerValue, 'information_schema')) {
|
||||
$analysis['database_fingerprint'] = 'mysql';
|
||||
} elseif (str_contains($lowerValue, 'postgres') || str_contains($lowerValue, 'pg_')) {
|
||||
$analysis['database_fingerprint'] = 'postgresql';
|
||||
} elseif (str_contains($lowerValue, 'mssql') || str_contains($lowerValue, 'sys.')) {
|
||||
$analysis['database_fingerprint'] = 'mssql';
|
||||
} elseif (str_contains($lowerValue, 'oracle') || str_contains($lowerValue, 'dual')) {
|
||||
$analysis['database_fingerprint'] = 'oracle';
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt benutzerfreundliche Fehlermeldung zurück (ohne Details zu verraten)
|
||||
*/
|
||||
public function getUserMessage(): string
|
||||
{
|
||||
return "Invalid input detected. Please check your data and try again.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt Wert für sicheres Logging
|
||||
*/
|
||||
private function sanitizeValueForLog(string $value): string
|
||||
{
|
||||
// Begrenzen auf 100 Zeichen und entferne gefährliche Zeichen
|
||||
$sanitized = substr($value, 0, 100);
|
||||
$sanitized = preg_replace('/[<>"\']/', '*', $sanitized);
|
||||
$sanitized = preg_replace('/\s+/', ' ', $sanitized);
|
||||
|
||||
return trim($sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt Empfehlung für Verteidigung zurück
|
||||
*/
|
||||
public function getDefenseRecommendation(): string
|
||||
{
|
||||
return match ($this->detectionMethod) {
|
||||
'union_detection' => 'Implement parameterized queries and input validation.',
|
||||
'stacked_queries_detection' => 'Disable multiple statements and use stored procedures.',
|
||||
'error_based_detection' => 'Implement proper error handling and logging.',
|
||||
'time_based_detection' => 'Add request timeouts and rate limiting.',
|
||||
'blind_detection' => 'Implement comprehensive input validation and WAF rules.',
|
||||
default => 'Use parameterized queries, input validation, and proper escaping.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert IOC (Indicator of Compromise) für Security-Teams
|
||||
*/
|
||||
public function generateIOC(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'sql_injection_attempt',
|
||||
'source_ip' => $this->clientIp,
|
||||
'target_field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'detection_method' => $this->detectionMethod,
|
||||
'severity' => $this->getAttackSeverity(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'automated' => $this->isAutomatedAttack(),
|
||||
'analysis' => $this->analyzePattern(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sofortige Gegenmaßnahmen erforderlich sind
|
||||
*/
|
||||
public function requiresImmediateAction(): bool
|
||||
{
|
||||
return in_array($this->getAttackSeverity(), ['HIGH', 'CRITICAL']) || $this->isAutomatedAttack();
|
||||
}
|
||||
}
|
||||
365
src/Framework/Exception/Security/XssAttemptException.php
Normal file
365
src/Framework/Exception/Security/XssAttemptException.php
Normal file
@@ -0,0 +1,365 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Exception\Security;
|
||||
|
||||
use App\Framework\Exception\ErrorCode;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
use App\Framework\Exception\FrameworkException;
|
||||
|
||||
/**
|
||||
* Ausnahme für XSS-Angriffs-Versuche
|
||||
*
|
||||
* Verwendet OWASP-konforme Nachrichten für XSS-Detection
|
||||
*/
|
||||
final class XssAttemptException extends FrameworkException
|
||||
{
|
||||
/**
|
||||
* @param string $clientIp Client-IP für Security-Tracking
|
||||
* @param string $field Betroffenes Feld/Parameter
|
||||
* @param string $pattern Erkanntes XSS-Pattern
|
||||
* @param string $originalValue Ursprünglicher Wert (sanitized)
|
||||
* @param string $xssType Art des XSS (reflected, stored, dom)
|
||||
* @param \Throwable|null $previous Vorherige Ausnahme
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly string $clientIp,
|
||||
public readonly string $field,
|
||||
public readonly string $pattern,
|
||||
public readonly string $originalValue,
|
||||
public readonly string $xssType = 'reflected',
|
||||
?\Throwable $previous = null
|
||||
) {
|
||||
// OWASP-konforme Nachricht mit Platzhaltern
|
||||
$message = "Client {$this->clientIp} attempted XSS attack in field {$this->field}";
|
||||
|
||||
$context = ExceptionContext::forOperation('security.xss_detection', 'Security')
|
||||
->withData([
|
||||
'client_ip' => $this->clientIp,
|
||||
'field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'xss_type' => $this->xssType,
|
||||
'value_length' => strlen($this->originalValue),
|
||||
'event_identifier' => "security_xss_attempt:{$this->clientIp},{$this->field}",
|
||||
'category' => 'input_validation',
|
||||
'requires_alert' => true, // XSS-Versuche erfordern immer Alerts
|
||||
])
|
||||
->withDebug([
|
||||
'sanitized_value' => $this->sanitizeValueForLog($this->originalValue),
|
||||
])
|
||||
->withMetadata([
|
||||
'security_event' => true,
|
||||
'owasp_compliant' => true,
|
||||
'log_level' => 'ERROR',
|
||||
'attack_type' => 'xss',
|
||||
'critical_security_event' => true,
|
||||
]);
|
||||
|
||||
parent::__construct(
|
||||
message: $message,
|
||||
context: $context,
|
||||
code: 400, // Bad Request
|
||||
previous: $previous,
|
||||
errorCode: ErrorCode::SECURITY_XSS_ATTEMPT
|
||||
);
|
||||
}
|
||||
|
||||
// === Factory Methods für verschiedene XSS-Patterns ===
|
||||
|
||||
public static function scriptTag(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, '<script> tag injection', $value, 'reflected');
|
||||
}
|
||||
|
||||
public static function onEventHandler(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Event handler injection (onclick, onload, etc.)', $value, 'reflected');
|
||||
}
|
||||
|
||||
public static function javascriptProtocol(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'javascript: protocol injection', $value, 'reflected');
|
||||
}
|
||||
|
||||
public static function htmlInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'HTML tag injection', $value, 'reflected');
|
||||
}
|
||||
|
||||
public static function domBasedXss(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'DOM-based XSS pattern', $value, 'dom');
|
||||
}
|
||||
|
||||
public static function storedXss(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'Stored XSS pattern', $value, 'stored');
|
||||
}
|
||||
|
||||
public static function cssInjection(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'CSS injection with expression()', $value, 'reflected');
|
||||
}
|
||||
|
||||
public static function svgXss(string $clientIp, string $field, string $value): self
|
||||
{
|
||||
return new self($clientIp, $field, 'SVG-based XSS injection', $value, 'reflected');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt OWASP-konforme Event-Daten zurück
|
||||
*/
|
||||
public function getSecurityEventData(): array
|
||||
{
|
||||
return [
|
||||
'event_identifier' => "security_xss_attempt:{$this->clientIp},{$this->field}",
|
||||
'description' => "Client {$this->clientIp} attempted XSS attack in field {$this->field}",
|
||||
'category' => 'input_validation',
|
||||
'log_level' => 'ERROR',
|
||||
'requires_alert' => true,
|
||||
'client_ip' => $this->clientIp,
|
||||
'field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'xss_type' => $this->xssType,
|
||||
'attack_severity' => $this->getAttackSeverity(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bestimmt Schweregrad des XSS-Angriffs
|
||||
*/
|
||||
public function getAttackSeverity(): string
|
||||
{
|
||||
return match ($this->xssType) {
|
||||
'stored' => 'CRITICAL', // Stored XSS ist am gefährlichsten
|
||||
'dom' => 'HIGH', // DOM-based XSS ist schwer zu erkennen
|
||||
'reflected' => 'MEDIUM', // Reflected XSS ist häufig aber weniger persistent
|
||||
default => 'MEDIUM',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* XSS-Versuche erfordern immer Alerts
|
||||
*/
|
||||
public function requiresAlert(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob es ein automatisierter Angriff ist
|
||||
*/
|
||||
public function isAutomatedAttack(): bool
|
||||
{
|
||||
$automatedPatterns = [
|
||||
'alert(',
|
||||
'prompt(',
|
||||
'confirm(',
|
||||
'document.cookie',
|
||||
'xss',
|
||||
'<svg',
|
||||
'javascript:',
|
||||
'onerror=',
|
||||
'onload=',
|
||||
];
|
||||
|
||||
$lowerValue = strtolower($this->originalValue);
|
||||
foreach ($automatedPatterns as $pattern) {
|
||||
if (str_contains($lowerValue, $pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analysiert das XSS-Pattern detailliert
|
||||
*/
|
||||
public function analyzePattern(): array
|
||||
{
|
||||
$analysis = [
|
||||
'vector_type' => 'unknown',
|
||||
'payload_complexity' => 'low',
|
||||
'encoding_used' => false,
|
||||
'obfuscation_detected' => false,
|
||||
'automation_detected' => $this->isAutomatedAttack(),
|
||||
'potential_impact' => 'low',
|
||||
];
|
||||
|
||||
$lowerValue = strtolower($this->originalValue);
|
||||
|
||||
// Vector-Typ-Erkennung
|
||||
if (str_contains($lowerValue, '<script')) {
|
||||
$analysis['vector_type'] = 'script_tag';
|
||||
} elseif (str_contains($lowerValue, 'on') && preg_match('/on\w+\s*=/', $lowerValue)) {
|
||||
$analysis['vector_type'] = 'event_handler';
|
||||
} elseif (str_contains($lowerValue, 'javascript:')) {
|
||||
$analysis['vector_type'] = 'javascript_protocol';
|
||||
} elseif (str_contains($lowerValue, '<svg') || str_contains($lowerValue, '<embed')) {
|
||||
$analysis['vector_type'] = 'svg_embed';
|
||||
} elseif (str_contains($lowerValue, 'expression(')) {
|
||||
$analysis['vector_type'] = 'css_expression';
|
||||
}
|
||||
|
||||
// Komplexität bewerten
|
||||
if (strlen($this->originalValue) > 100) {
|
||||
$analysis['payload_complexity'] = 'high';
|
||||
} elseif (strlen($this->originalValue) > 50) {
|
||||
$analysis['payload_complexity'] = 'medium';
|
||||
}
|
||||
|
||||
// Encoding-Erkennung
|
||||
if (str_contains($this->originalValue, '%') || str_contains($this->originalValue, '&#')) {
|
||||
$analysis['encoding_used'] = true;
|
||||
}
|
||||
|
||||
// Obfuskierung-Erkennung
|
||||
if (preg_match('/String\.fromCharCode|eval\(|unescape\(/', $this->originalValue)) {
|
||||
$analysis['obfuscation_detected'] = true;
|
||||
$analysis['payload_complexity'] = 'high';
|
||||
}
|
||||
|
||||
// Impact-Bewertung
|
||||
if (str_contains($lowerValue, 'cookie') || str_contains($lowerValue, 'document')) {
|
||||
$analysis['potential_impact'] = 'high';
|
||||
} elseif (str_contains($lowerValue, 'alert') || str_contains($lowerValue, 'prompt')) {
|
||||
$analysis['potential_impact'] = 'medium';
|
||||
}
|
||||
|
||||
return $analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt benutzerfreundliche Fehlermeldung zurück (ohne Details zu verraten)
|
||||
*/
|
||||
public function getUserMessage(): string
|
||||
{
|
||||
return "Invalid input detected. Please check your data and try again.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Bereinigt Wert für sicheres Logging
|
||||
*/
|
||||
private function sanitizeValueForLog(string $value): string
|
||||
{
|
||||
// HTML-Entities kodieren und auf 100 Zeichen begrenzen
|
||||
$sanitized = htmlspecialchars(substr($value, 0, 100), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
// Zusätzliche Bereinigung für Logs
|
||||
$sanitized = preg_replace('/\s+/', ' ', $sanitized);
|
||||
|
||||
return trim($sanitized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt spezifische Verteidigungsempfehlung zurück
|
||||
*/
|
||||
public function getDefenseRecommendation(): string
|
||||
{
|
||||
return match ($this->xssType) {
|
||||
'stored' => 'Implement output encoding, Content Security Policy, and input validation.',
|
||||
'dom' => 'Use safe JavaScript APIs, validate DOM manipulation, implement CSP.',
|
||||
'reflected' => 'Implement output encoding, input validation, and Content Security Policy.',
|
||||
default => 'Use output encoding, input validation, and Content Security Policy.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert Content Security Policy Empfehlungen
|
||||
*/
|
||||
public function getCspRecommendations(): array
|
||||
{
|
||||
$baseCSP = [
|
||||
"default-src 'self'",
|
||||
"script-src 'self'",
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"font-src 'self'",
|
||||
"object-src 'none'",
|
||||
"frame-src 'none'",
|
||||
];
|
||||
|
||||
$analysis = $this->analyzePattern();
|
||||
|
||||
// Verschärfungen basierend auf Angriffsmuster
|
||||
if ($analysis['vector_type'] === 'script_tag') {
|
||||
$baseCSP[] = "script-src 'self' 'nonce-{random}'"; // Nonce-basierte Scripts
|
||||
}
|
||||
|
||||
if ($analysis['vector_type'] === 'css_expression') {
|
||||
$baseCSP[] = "style-src 'self'"; // Entferne unsafe-inline
|
||||
}
|
||||
|
||||
return $baseCSP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert IOC (Indicator of Compromise) für Security-Teams
|
||||
*/
|
||||
public function generateIOC(): array
|
||||
{
|
||||
return [
|
||||
'type' => 'xss_attempt',
|
||||
'source_ip' => $this->clientIp,
|
||||
'target_field' => $this->field,
|
||||
'pattern' => $this->pattern,
|
||||
'xss_type' => $this->xssType,
|
||||
'severity' => $this->getAttackSeverity(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'automated' => $this->isAutomatedAttack(),
|
||||
'analysis' => $this->analyzePattern(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob sofortige Gegenmaßnahmen erforderlich sind
|
||||
*/
|
||||
public function requiresImmediateAction(): bool
|
||||
{
|
||||
return $this->xssType === 'stored' ||
|
||||
$this->getAttackSeverity() === 'CRITICAL' ||
|
||||
$this->isAutomatedAttack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt WAF (Web Application Firewall) Regel-Vorschläge zurück
|
||||
*/
|
||||
public function getWafRuleSuggestions(): array
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
$analysis = $this->analyzePattern();
|
||||
|
||||
switch ($analysis['vector_type']) {
|
||||
case 'script_tag':
|
||||
$rules[] = 'Block requests containing <script tags in input fields';
|
||||
|
||||
break;
|
||||
case 'event_handler':
|
||||
$rules[] = 'Block requests containing on* event handlers in input fields';
|
||||
|
||||
break;
|
||||
case 'javascript_protocol':
|
||||
$rules[] = 'Block requests containing javascript: protocol in input fields';
|
||||
|
||||
break;
|
||||
case 'svg_embed':
|
||||
$rules[] = 'Block or sanitize SVG/embed tags in user input';
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($analysis['encoding_used']) {
|
||||
$rules[] = 'Implement URL decoding before XSS detection';
|
||||
}
|
||||
|
||||
if ($analysis['obfuscation_detected']) {
|
||||
$rules[] = 'Implement advanced obfuscation detection rules';
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user