<?php
class WAF
{
private $nonce;
private $headers = [];
private $blockedIPs = [];
private $rateLimit = [];
private $session_config = [];
private $maxRequestsPerMinute = 60;
private $storageFile;
private $max_attempts;
private $lockout_time;
private $anomalyThreshold = 5;

private $sqlInjectionPatterns = [
'/union\s+select/i',
'/select.*from/i',
'/insert\s+into/i',
'/update.*set/i',
'/delete\s+from/i',
'/drop\s+table/i',
'/or\s+1=1/i',
'/exec(\s|\()+/i',
'/--\s+/',
'/;/',
'/\/\*.*\*\//'
];

private $xssPatterns = [
'/<script.*?>.*?<\/script>/is',
'/javascript:/i',
'/onload\s*=/i',
'/onerror\s*=/i',
'/onclick\s*=/i',
'/<iframe.*?>/is',
'/<object.*?>/is',
'/<embed.*?>/is'
];

private $rcePatterns = [
'/system\s*\(/i',
'/exec\s*\(/i',
'/shell_exec\s*\(/i',
'/eval\s*\(/i',
'/passthru\s*\(/i',
'/`.*`/',
'/\$\{.*\}/'
];

public function __construct($config = [])
{
if (isset($config['maxRequestsPerMinute'])) {
$this->maxRequestsPerMinute = $config['maxRequestsPerMinute'];
}
if (isset($config['nonce'])) {
$this->nonce = $config['nonce'];
}
$this->runSecurityChecks();
$this->update_csp_header();
$this->update_session_config();
$this->startSecureSession();
$this->storageFile = __DIR__ . '/logs/bruteforce.waf_log';
$this->lockout_time = 900;
$this->max_attempts = 2;
}

private function update_csp_header() {
$this->headers = [
'X-Frame-Options' => 'DENY',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'Referrer-Policy' => 'strict-origin-when-cross-origin',
'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
'Permissions-Policy' => 'geolocation=(), microphone=()'
];

if (headers_sent()) {
error_log('Security Warning: Headers already sent before HeaderEnforcer could set them');
return;
}

foreach ($this->headers as $name => $value) {
header("$name: $value");
}

header_remove('X-Powered-By');
header_remove('Server');

}

private function update_session_config() {
$this->session_config  = [
'cookie_httponly' => true,
'cookie_secure' => false,
'entropy_file' => '/dev/urandom',
'hash_function' => 'sha256',
//'secret_salt' => bin2hex(random_bytes(32))
'secret_salt' => 'static-dev-salt'
];

session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $_SERVER['HTTP_HOST'] ?? '',
'secure' => $this->session_config['cookie_secure'],
'httponly' => $this->session_config['cookie_httponly'],
'samesite' => 'Strict'
]);

ini_set('session.use_strict_mode', '1');
ini_set('session.use_only_cookies', '1');
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', $this->session_config['cookie_secure'] ? '1' : '0');
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.hash_function', $this->session_config['hash_function']);

if (!empty($this->session_config['entropy_file'])) {
ini_set('session.entropy_file', $this->session_config['entropy_file']);
ini_set('session.entropy_length', '32');
}

}


public function startSecureSession(): void {
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}

session_start();

if (empty($_SESSION['initiated'])) {
session_regenerate_id(true);
$_SESSION['initiated'] = true;
$_SESSION['fingerprint'] = $this->generateFingerprint();
}

if ($_SESSION['fingerprint'] !== $this->generateFingerprint()) {
$this->handleHijack();
}
}

private function generateFingerprint(): string {
return hash('sha256',
$_SERVER['HTTP_USER_AGENT'].
(isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '').
$this->session_config['secret_salt']
);
}

private function handleHijack(): void {
session_regenerate_id(true);
session_destroy();

$context = sprintf(
'IP: '.$_SERVER['REMOTE_ADDR'].' UA: '.
$_SERVER['HTTP_USER_AGENT']
);

$this->blockRequest("Session hijack attempt detected in $context", 403, uniqid());
}

public function regenerateId(): void {
session_regenerate_id(true);
$_SESSION['fingerprint'] = $this->generateFingerprint();
}

public function destroySession(): void {
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params["path"],
$params["domain"],
$params["secure"],
$params["httponly"]
);
}
session_destroy();
}


private function runSecurityChecks()
{
$this->checkRateLimit();

if (!empty($_GET)) {
$this->scanInput($_GET, 'GET');
}

if (!empty($_POST)) {
$this->scanInput($_POST, 'POST');
}

if (!empty($_COOKIE)) {
$this->scanInput($_COOKIE, 'COOKIE');
}

$this->checkHeaders();
}

private function checkRateLimit()
{
$clientIP = $this->getClientIP();
$currentTime = time();
$minuteWindow = floor($currentTime / 60);

if (!isset($this->rateLimit[$clientIP])) {
$this->rateLimit[$clientIP] = [
'window' => $minuteWindow,
'count' => 1
];
return;
}

if ($this->rateLimit[$clientIP]['window'] != $minuteWindow) {
$this->rateLimit[$clientIP] = [
'window' => $minuteWindow,
'count' => 1
];
return;
}

$this->rateLimit[$clientIP]['count']++;

if ($this->rateLimit[$clientIP]['count'] > $this->maxRequestsPerMinute) {
$this->blockRequest('Rate limit exceeded', 429, uniqid());
}
}

private function scanInput($input, $type)
{
foreach ($input as $key => $value) {
if (is_array($value)) {
$this->scanInput($value, $type);
} else {
$this->checkForMaliciousContent($value, $type . ' parameter: ' . $key);
}
}
}

private function checkForMaliciousContent($input, $context)
{

foreach ($this->sqlInjectionPatterns as $pattern) {
if (preg_match($pattern, $input)) {
$this->blockRequest("SQL injection detected in $context", 403, uniqid());
}
}

foreach ($this->xssPatterns as $pattern) {
if (preg_match($pattern, $input)) {
$this->blockRequest("XSS attempt detected in $context", 403, uniqid());
}
}

foreach ($this->rcePatterns as $pattern) {
if (preg_match($pattern, $input)) {
$this->blockRequest("RCE attempt detected in $context", 403, uniqid());
}
}

if (preg_match('/\.\.\//', $input) || preg_match('/\.\.\\\/', $input)) {
$this->blockRequest("Path traversal attempt detected in $context", 403, uniqid());
}
}

private function checkHeaders()
{
$headers = $this->getAllHeaders();

foreach ($headers as $header => $value) {
$lowerHeader = strtolower($header);
if (in_array($lowerHeader, [
'sec-ch-ua',
'sec-ch-ua-mobile',
'sec-ch-ua-platform',
'user-agent',
'accept',
'accept-language',
'accept-encoding',
'connection',
'cache-control',
'cookie',
'content-type',
'content-length',
'origin',
'referer',
'sec-fetch-site',
'sec-fetch-mode',
'sec-fetch-dest',
'upgrade-insecure-requests'
])) {
continue;
}

$this->checkForMaliciousContent($value, "Header: $header");
}
}

private function getAllHeaders()
{
if (function_exists('getallheaders')) {
return getallheaders();
}

$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
}
}
return $headers;
}

private function getClientIP()
{
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
return trim($ips[0]);
} elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
return $_SERVER['HTTP_X_REAL_IP'];
} else {
return $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
}
}

public function analyzeRequest(): void {
$score = 0;
$reasons = [];

if ($this->isSuspiciousUserAgent($_SERVER['HTTP_USER_AGENT'])) {
$score += 3;
$reasons[] = 'Suspicious User Agent';
}

if ($this->isAbnormalRequest()) {
$score += 2;
$reasons[] = 'Abnormal Request Pattern';
}

$attackFound = $this->scanInputs(array_merge($_GET, $_POST, $_COOKIE));
if ($attackFound) {
$score += 5;
$reasons[] = 'Attack Signature Detected';
}

if ($this->isHighRequestRate($_SERVER['REMOTE_ADDR'])) {
$score += 4;
$reasons[] = 'High Request Rate';
}

if ($score >= $this->anomalyThreshold) {
$this->logAnomaly($score, $reasons);
}
}

private function isSuspiciousUserAgent(string $userAgent): bool {
$suspicious = [
'nmap', 'sqlmap', 'nikto', 'metasploit',
'wget', 'curl', 'libwww', 'python-requests'
];

foreach ($suspicious as $pattern) {
if (stripos($userAgent, $pattern) !== false) {
return true;
}
}
return false;
}

private function isAbnormalRequest(): bool {

foreach ($_REQUEST as $param => $value) {
if (strlen($value) > 1000) {
return true;
}
}

return !in_array($_SERVER['REQUEST_METHOD'], ['GET', 'POST', 'HEAD']);
}


public function checkAttempt(string $login_email): bool {
$ip = $_SERVER['REMOTE_ADDR'];
$key = md5($ip.$login_email);
$attempts = $this->getAttempts($key);

if ($attempts >= $this->max_attempts) {
$this->blockRequest('BRUTE_FORCE_BLOCK - IP: '.$ip.' | Username: '.$login_email, 403, uniqid());
}
return true;
}

private function getAttempts(string $key): int {
$data = $this->loadAttemptData();
if (isset($data[$key])) {

if (time() - $data[$key]['last_attempt'] > $this->lockout_time) {
return 0;
}
return $data[$key]['attempts'];
}
return 0;
}

private function loadAttemptData(): array {
if (!file_exists($this->storageFile)) {
return [];
}
return json_decode(file_get_contents($this->storageFile), true) ?? [];
}

public function recordFailedAttempt(string $username): void {
$ip = $_SERVER['REMOTE_ADDR'];
$key = md5($ip.$username);
$attempts = $this->getAttempts($key) + 1;

$data = $this->loadAttemptData();
$data[$key] = [
'attempts' => $attempts,
'last_attempt' => time(),
'ip' => $ip,
'username' => $username
];

file_put_contents($this->storageFile, json_encode($data));
}

private function blockRequest($reason, $httpCode = 403, $reference)
{
$this->logAttackAttempt($reason, $reference);

http_response_code($httpCode);

echo "<!DOCTYPE html>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>Security Violation Detected</title>
<style nonce=".$this->nonce.">
:root {
--primary-color: #667eea;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--text-dark: #2d3436;
--text-light: #636e72;
--bg-light: #f8f9fa;
--border-color: #dfe6e9;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: segoe ui;
text-align: center;
padding: 20px;
background: #667eea;
display: flex;
align-items: center;
justify-content: center;
margin: 0;
line-height: 1.6;
}

.security-alert {
background: white;
color: var(--text-dark);
padding: 40px;
border-radius: 16px;
box-shadow: var(--shadow);
max-width: 580px;
width: 100%;
position: relative;
overflow: hidden;
}

.security-alert:before {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
}

.alert-icon {
font-size: 3.5rem;
margin-bottom: 20px;
display: block;
}

.security-alert h1 {
color: var(--danger-color);
margin-bottom: 15px;
font-size: 1.8rem;
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}

.security-alert p {
margin: 15px 0;
line-height: 1.6;
color: var(--text-light);
}

.reason {
background: #fff9f9;
border: 1px solid #ffebee;
padding: 18px;
border-radius: 10px;
margin: 20px 0;
font-family: consolas;
font-weight:bold;
font-size: 0.95rem;
color: var(--danger-color);
text-align: left;
position: relative;
}

.reason:before {
position: absolute;
left: -30px;
top: 50%;
transform: translateY(-50%);
font-size: 1.2rem;
}

.actions {
display: flex;
gap: 15px;
margin: 25px 0;
justify-content: center;
flex-wrap: wrap;
}

.btn {
padding: 12px 24px;
border-radius: 8px;
border: none;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.95rem;
display: inline-flex;
align-items: center;
gap: 8px;
}

.btn-primary {
background: var(--primary-color);
color: white;
}

.btn-secondary {
background: var(--bg-light);
color: var(--text-dark);
border: 1px solid var(--border-color);
}

.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.btn-primary:hover {
background: #5a6fd8;
}

.contact {
font-size: 0.85rem;
color: var(--text-light);
margin-top: 25px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
}

.reference-id {
font-weight: 600;
color: var(--text-dark);
}

@media (max-width: 600px) {
.security-alert {
padding: 30px 20px;
}

.actions {
flex-direction: column;
}

.contact {
flex-direction: column;
text-align: center;
}
}
</style>
</head>
<body>
<div class='security-alert'>
<h1>Security Violation Detected</h1>
<p>Our security system has identified and blocked a potentially harmful request from your connection.</p>

<div class='reason'>
<strong>Reason:</strong> ".$reason."
</div>

<p>This could be due to unusual traffic patterns or a potential security threat. If you believe this is an error, please contact our support team.</p>

<div class='contact'>
<div>Reference ID: <span class='reference-id'>$reference</span></div>
<div>Timestamp: ".date('Y-m-d H:i:s')."</div>
</div>
</div>
</body>
</html>

";

exit;
}

public function csrf_generate($action = null, $expiry = 600) {
if (!isset($_SESSION['csrf_tokens']) || !is_array($_SESSION['csrf_tokens'])) {
$_SESSION['csrf_tokens'] = [];
}

foreach ($_SESSION['csrf_tokens'] as $id => $data) {
if (is_array($data) && isset($data['expires']) && $data['expires'] < time()) {
unset($_SESSION['csrf_tokens'][$id]);
}
}

$formId = bin2hex(random_bytes(8));
$token  = bin2hex(random_bytes(32));
$expiresAt = time() + $expiry;

$_SESSION['csrf_tokens'][$formId] = [
'token' => $token,
'expires' => $expiresAt,
'action' => $action
];

return ['token' => $token, 'formId' => $formId];
}


public function csrf_validate($formId, $token, $action = null, $oneTimeUse = true) {
if (!isset($_SESSION['csrf_tokens'][$formId])) return false;

$stored = $_SESSION['csrf_tokens'][$formId];

if ($stored['expires'] < time()) {
unset($_SESSION['csrf_tokens'][$formId]);
return false;
}

if (!hash_equals($stored['token'], $token)) return false;

if ($action && (!isset($stored['action']) || $stored['action'] !== $action)) {
return false;
}

if ($oneTimeUse) unset($_SESSION['csrf_tokens'][$formId]);

return true;
}


public function csrf_field($action, $expiry) {
$csrf = $this->csrf_generate($action, $expiry);
return '<input type="hidden" name="csrf_token" value="' . $csrf['token'] . '">' .
'<input type="hidden" name="csrf_form_id" value="' . $csrf['formId'] . '">';
}

public function action_name(): string {
if (php_sapi_name() === 'cli') {
return basename($_SERVER['argv'][0] ?? '');
}

$candidates = [
'SCRIPT_NAME',
'PHP_SELF',
'SCRIPT_FILENAME',
'REQUEST_URI'
];

foreach ($candidates as $key) {
if (empty($_SERVER[$key])) continue;
$path = $_SERVER[$key];

if ($key === 'REQUEST_URI') {
$path = parse_url($path, PHP_URL_PATH) ?: $path;
}

$name = basename($path);
if ($name !== '' && $name !== '/' && strpos($name, '.') !== false) {
return $name;
}
}

return '';
}

private function logAttackAttempt($reason, $reference)
{
$clientIP = $this->getClientIP();
$timestamp = date('Y-m-d H:i:s');
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'Unknown';
$requestUri = $_SERVER['REQUEST_URI'] ?? 'Unknown';

$logEntry = "[$timestamp] IP: $clientIP | Reference: $reference | Reason: $reason | URI: $requestUri | User-Agent: $userAgent" . PHP_EOL;

$logFile = __DIR__ . '/logs/' . date('Y-m-d') . '.waf_log';
if (!is_dir(dirname($logFile))) {
mkdir(dirname($logFile), 0755, true);
}

file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
}

public function blockIP($ip, $reason = "Manual block")
{
$this->blockedIPs[$ip] = [
'timestamp' => time(),
'reason' => $reason
];

$clientIP = $this->getClientIP();
if ($clientIP === $ip) {
$this->blockRequest($reason, 403, uniqid());
}
}

public function isIPBlocked($ip)
{
return isset($this->blockedIPs[$ip]);
}
}


?>
