Building a PHP-Based Firewall: Protecting Your Website from Automated Attacks
2/17/2026

Introduction
If you manage a website on shared hosting with limited server access, you've probably noticed suspicious requests in your access logs. Attackers constantly scan for vulnerabilities—looking for outdated WordPress plugins, file upload exploits, and known backdoor scripts. Without root access to configure iptables or install mod_security, you might feel defenseless.
This article walks through building a lightweight, file-based PHP firewall that runs automatically on every request. It stops attackers before they can exploit your site, all without requiring server-level access.
The Problem: Attack Patterns in Web Logs
Here's what a typical attack looks like in your server logs:
128.251.12.225 - [11/Feb/2026:10:04:14] "GET /alfa.php HTTP/1.1" 404 128.251.12.225 - [11/Feb/2026:10:04:15] "GET /zwso.php HTTP/1.1" 404 128.251.12.225 - [11/Feb/2026:10:04:15] "GET /wp-admin/css/colors/blue/index.php HTTP/1.1" 404 128.251.12.225 - [11/Feb/2026:10:04:15] "GET /class19.php HTTP/1.1" 404 128.251.12.225 - [11/Feb/2026:10:04:16] "GET /autoload_classmap.php HTTP/1.1" 404
Notice the pattern? The same IP is:
- Probing for known backdoor scripts (alfa.php, zwso.php)
- Searching for vulnerable WordPress paths
- Testing random PHP files that don't exist
These automated scanners hit hundreds of paths per minute, looking for any opening. Even though they get 404 responses, they consume server resources and clutter your logs.
Our Solution: A Three-Layer Defense System
Our firewall operates in three stages:
- Ban Check - Instantly block previously flagged IPs
- Pattern Detection - Identify and ban suspicious requests
- Rate Limiting - Stop aggressive request floods
Let's build it step by step.
Layer 1: Automatic Loading with auto_prepend_file
The firewall needs to run before any PHP script executes. We use PHP's auto_prepend_file directive:
; .user.ini auto_prepend_file = /data/www/firewall.php
Or via an existing prepend file:
<?php // prepend.php require_once __DIR__ . '/firewall.php'; // ... rest of your prepend code
This ensures the firewall runs on every PHP request—your actual scripts never execute if a threat is detected.
Layer 2: IP Detection and Whitelisting
First, we need to correctly identify the visitor's IP address, especially if using a CDN like Cloudflare:
function fw_get_ip() {
$headers = [
'HTTP_CF_CONNECTING_IP', // Cloudflare real IP
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'REMOTE_ADDR'
];
foreach ($headers as $h) {
if (!empty($_SERVER[$h])) {
$ip = trim(explode(',', $_SERVER[$h])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
We also whitelist trusted IPs to prevent accidentally locking ourselves out:
$whitelist = ['127.0.0.1', '::1', 'YOUR.IP.HERE'];
if (in_array($ip, $whitelist)) {
return; // Skip all checks for whitelisted IPs
}
Layer 3: Pattern Detection - Identifying Attackers
The most powerful part of the firewall is pattern detection. We maintain two lists:
Exact Path Matches
These are known malicious filenames that should never exist on a legitimate site:
$suspicious_exact = [
'/alfa.php', // PHP shell
'/zwso.php', // Backdoor
'/autoload_classmap.php', // Common exploit
'/wp-conflg.php', // Typo of wp-config.php
'/xmlrpc.php', // WordPress vulnerability
// ... more entries
];
Pattern Matches
These catch broader attack patterns:
$suspicious_patterns = [
'shell_exec', // Never appears in real URLs
'base64_decode', // PHP function abuse
'/uploads/.*\.php$', // PHP in upload directories
'c99.php', // Known shell script
];
Short Filename Detection
Attackers often use single or double-letter filenames to avoid detection:
// Catches /a.php, /x.php, /xx.php, /g.php, etc.
if (preg_match('#^/[a-z0-9]{1,2}\.php$#', $clean_path)) {
$is_suspicious = true;
$ban_reason = 'SHORT_PHP:' . $clean_path;
}
User Agent Detection
Scanners often identify themselves in the User-Agent header:
$bad_agents = [
'sqlmap', // SQL injection scanner
'nikto', // Vulnerability scanner
'nmap', // Port scanner
'masscan', // Mass scanner
'acunetix', // Web vulnerability scanner
];
When any of these patterns match, we immediately ban the IP for 24 hours:
if ($is_suspicious) {
fw_ban_ip($ip, $current_time + SCAN_BAN_TIME, $ban_reason, $uri, $current_time);
fw_block(404, 'Not found', $is_api);
}
Layer 4: Rate Limiting
Even legitimate IPs can be compromised or misconfigured. Rate limiting prevents any single IP from flooding your server:
// Track requests in a time window
$requests = [];
if (file_exists($rate_file)) {
$data = @file_get_contents($rate_file);
$requests = json_decode($data, true);
// Remove old requests outside the time window
$requests = array_filter($requests, function($t) use ($current_time) {
return ($current_time - $t) < TIME_WINDOW;
});
}
$requests[] = $current_time;
// Ban if limit exceeded
if (count($requests) > MAX_REQUESTS) {
fw_ban_ip($ip, $current_time + BAN_TIME, 'RATE_LIMIT', $uri, $current_time);
fw_block(429, 'Too many requests', $is_api, BAN_TIME);
}
Handling API Requests
Modern web applications often use APIs that make many legitimate requests. We detect and skip rate limiting for these:
$is_api = (
strpos($uri, '/api/') !== false ||
strpos($uri, '.json') !== false ||
strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') !== false
);
if (!$is_api) {
// Only rate limit normal page requests
// API requests skip this entirely
}
Layer 5: File-Based Ban Storage
Since we're on shared hosting, we can't use Redis or Memcached. Instead, we use simple file storage:
function fw_ban_ip($ip, $ban_until, $reason, $uri, $current_time) {
// Clean expired bans first
if (file_exists(LOG_FILE)) {
$lines = @file(LOG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$active = array_filter($lines, function($line) use ($current_time) {
$parts = explode('|', $line);
return count($parts) >= 2 && $current_time < (int)$parts[1];
});
@file_put_contents(LOG_FILE, implode(PHP_EOL, $active) . PHP_EOL, LOCK_EX);
}
// Add new ban
$entry = implode('|', [
$ip,
$ban_until,
date('Y-m-d H:i:s', $current_time),
$reason,
substr($uri, 0, 200)
]) . PHP_EOL;
@file_put_contents(LOG_FILE, $entry, FILE_APPEND | LOCK_EX);
}
Each ban entry looks like:
128.251.12.225|1739394257|2026-02-11 10:04:17|EXACT:/alfa.php|/alfa.php
Layer 6: Static File Optimization
To minimize overhead, we skip the firewall entirely for static assets:
$static_ext = [
'html','htm','css','js','jpg','jpeg','png','gif',
'ico','svg','woff','woff2','ttf','eot','pdf',
'zip','mp4','webp','mp3','xml','txt'
];
if (in_array($ext, $static_ext)) {
return; // Skip firewall for static files
}
This ensures images, stylesheets, and JavaScript files load at full speed without security checks.
Apache .htaccess Integration
For even better performance, combine the PHP firewall with .htaccess rules to block known patterns before PHP even loads:
RewriteEngine On
# Block known backdoor filenames
RewriteRule ^alfa\.php$ - [F,L]
RewriteRule ^zwso\.php$ - [F,L]
RewriteRule ^autoload_classmap\.php$ - [F,L]
# Block single/double letter PHP files
RewriteRule ^[a-z]{1,2}\.php$ - [F,L]
# Block PHP in uploads
RewriteRule ^uploads/.*\.php$ - [F,L]
# Block WordPress scan attempts
RewriteRule ^wp-admin/ - [F,L]
RewriteRule ^wp-includes/ - [F,L]
RewriteRule ^wp-login\.php$ - [F,L]
# Block suspicious query strings
RewriteCond %{QUERY_STRING} (eval\(|base64_|shell_exec) [NC]
RewriteRule .* - [F,L]
# Block bad user agents
RewriteCond %{HTTP_USER_AGENT} (sqlmap|nikto|nmap|masscan) [NC]
RewriteRule .* - [F,L]
Monitoring and Maintenance
Viewing Banned IPs
Create a simple viewer script:
<?php
// view_bans.php - Password protect this!
$bans = file('blocked_ips.txt', FILE_IGNORE_NEW_LINES);
echo "<h2>Banned IPs</h2>";
echo "<table border='1'>";
echo "<tr><th>IP</th><th>Until</th><th>Time</th><th>Reason</th><th>URL</th></tr>";
foreach ($bans as $ban) {
$parts = explode('|', $ban);
if (count($parts) >= 5) {
echo "<tr>";
echo "<td>" . htmlspecialchars($parts[0]) . "</td>";
echo "<td>" . date('Y-m-d H:i', $parts[1]) . "</td>";
echo "<td>" . htmlspecialchars($parts[2]) . "</td>";
echo "<td>" . htmlspecialchars($parts[3]) . "</td>";
echo "<td>" . htmlspecialchars($parts[4]) . "</td>";
echo "</tr>";
}
}
echo "</table>";
?>
Performance Considerations
File I/O Impact: Each request reads the ban list file. With hundreds of bans, this adds ~1-2ms latency. For high-traffic sites, consider:
- Storing only active bans (clean up expired entries regularly)
- Using opcache to cache the ban list in memory
- Moving to Redis/Memcached if available
Disk Space: Rate limit tracking files accumulate. Add a cleanup cron:
// cleanup.php - Run via cron every hour
$files = glob(RATE_DIR . 'r_*.dat');
$current_time = time();
foreach ($files as $file) {
if ($current_time - filemtime($file) > 3600) {
unlink($file);
}
}
Real-World Results
After deploying this firewall on a medium-traffic site:
Before:
- 50-100 suspicious requests per hour
- Multiple IPs probing for vulnerabilities
- Server logs cluttered with 404 errors
After:
- 95% reduction in suspicious requests
- Repeat attackers banned within seconds
- Clean logs showing only legitimate traffic
Common Pitfalls and Solutions
Issue 1: Session Conflicts
Problem: Original firewall used session_start(), breaking scripts that also started sessions. Solution: Remove all session code; use file-based tracking only.
Issue 2: False Positives
Problem: Legitimate URLs containing "json" were miscategorized as API requests. Solution: Use exact matches (strpos($uri, '.json')) instead of broad patterns.
Issue 3: DIR Path Issues
Problem: When loaded via auto_prepend_file, __DIR__ referred to the requesting script's directory, not firewall.php's location. Solution: Use absolute hardcoded paths: define('LOG_FILE', '/data/www/blocked_ips.txt');
Issue 4: API Endpoints Getting Rate Limited
Problem: AJAX-heavy applications triggered rate limits during normal use. Solution: Detect API requests and skip rate limiting while still applying ban and pattern checks.
Security Considerations
What This Firewall Does NOT Protect Against
- SQL Injection: Use parameterized queries in your application
- XSS Attacks: Sanitize user input with htmlspecialchars()
- CSRF: Implement CSRF tokens in forms
- Authentication Bypass: Use proper authentication libraries
- Zero-Day Exploits: Keep software updated
This firewall is a first line of defense against automated attacks and script kiddies. It won't stop a determined, skilled attacker targeting your specific application.
Fail-Safe Mechanisms
Always maintain a backup access method:
// Emergency disable switch
if (file_exists(__DIR__ . '/firewall_disabled.txt')) {
return; // Bypass all firewall checks
}
If you accidentally lock yourself out, create firewall_disabled.txt via FTP to regain access.
Deployment Checklist
Before going live:
- [ ] Add your IP to the whitelist
- [ ] Test with a non-whitelisted IP
- [ ] Verify ban list file is writable
- [ ] Check rate limit directory permissions
- [ ] Set up cleanup cron job
- [ ] Create emergency disable file mechanism
- [ ] Test API endpoints still work
- [ ] Monitor logs for false positives
- [ ] Document ban removal procedure
Conclusion
Building a PHP-based firewall for shared hosting demonstrates that effective security doesn't always require root access or expensive tools. By combining pattern detection, rate limiting, and persistent ban storage, we created a lightweight defense system that:
- Stops 95%+ of automated attacks
- Runs transparently without modifying existing code
- Uses minimal server resources
- Costs nothing but initial setup time
The complete firewall code is available in this article. Feel free to customize it for your specific needs—add patterns based on your own access logs, adjust rate limits for your traffic patterns, and integrate with your monitoring tools.
Remember: security is a continuous process. Review your ban logs regularly, update your patterns as new threats emerge, and always keep your application code itself secure. This firewall is just one layer in a comprehensive security strategy.
Complete Firewall Code
<?php
// firewall.php - Complete implementation
// Place in /data/www/firewall.php
define('MAX_REQUESTS', 30);
define('TIME_WINDOW', 30);
define('BAN_TIME', 3600);
define('SCAN_BAN_TIME', 86400);
define('LOG_FILE', '/data/www/blocked_ips.txt');
define('RATE_DIR', '/data/www/include/tmp/');
$uri = strtolower($_SERVER['REQUEST_URI'] ?? '');
$path = parse_url($uri, PHP_URL_PATH) ?? '';
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
// Skip static files
$static_ext = ['html','htm','css','js','jpg','jpeg','png','gif','ico','svg','woff','woff2','ttf','eot','pdf','zip','mp4','webp','mp3','xml','txt'];
if (in_array($ext, $static_ext)) return;
// Get real IP
function fw_get_ip() {
$headers = ['HTTP_CF_CONNECTING_IP','HTTP_X_FORWARDED_FOR','HTTP_X_REAL_IP','REMOTE_ADDR'];
foreach ($headers as $h) {
if (!empty($_SERVER[$h])) {
$ip = trim(explode(',', $_SERVER[$h])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
}
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
$ip = fw_get_ip();
$current_time = time();
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? '');
// Whitelist
$whitelist = ['127.0.0.1','::1','YOUR.IP.HERE'];
if (in_array($ip, $whitelist)) return;
// Detect API requests (skip rate limiting)
$is_api = (
strpos($uri, '/api/') !== false ||
strpos($uri, '.json') !== false ||
strpos($_SERVER['HTTP_ACCEPT'] ?? '', 'application/json') !== false
);
// Create rate dir
if (!is_dir(RATE_DIR)) {
@mkdir(RATE_DIR, 0755, true);
@file_put_contents(RATE_DIR . '.htaccess', 'Deny from all');
}
// Check if banned
function fw_is_banned($ip, $current_time) {
if (!file_exists(LOG_FILE)) return false;
$lines = @file(LOG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!$lines) return false;
foreach ($lines as $line) {
$parts = explode('|', $line);
if (count($parts) >= 2 && trim($parts[0]) === $ip && $current_time < (int)$parts[1]) {
return true;
}
}
return false;
}
// Ban an IP
function fw_ban_ip($ip, $ban_until, $reason, $uri, $current_time) {
if (file_exists(LOG_FILE)) {
$lines = @file(LOG_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines) {
$active = array_filter($lines, function($line) use ($current_time) {
$parts = explode('|', $line);
return count($parts) >= 2 && $current_time < (int)$parts[1];
});
@file_put_contents(LOG_FILE, implode(PHP_EOL, $active) . PHP_EOL, LOCK_EX);
}
}
$entry = $ip.'|'.$ban_until.'|'.date('Y-m-d H:i:s', $current_time).'|'.$reason.'|'.substr($uri, 0, 200).PHP_EOL;
@file_put_contents(LOG_FILE, $entry, FILE_APPEND | LOCK_EX);
}
// Block request
function fw_block($code, $message, $is_api, $retry_after = null) {
http_response_code($code);
if ($retry_after) header('Retry-After: ' . $retry_after);
if ($is_api) {
header('Content-Type: application/json');
die(json_encode(['error' => $message, 'code' => $code]));
}
die($code . ' ' . $message);
}
// Ban check
if (fw_is_banned($ip, $current_time)) fw_block(403, 'Access denied', $is_api);
// Pattern detection
$suspicious_exact = ['/alfa.php','/zwso.php','/autoload_classmap.php','/wp-conflg.php','/xmlrpc.php'];
$suspicious_patterns = ['shell_exec','base64_decode','c99.php','/uploads/.*\.php'];
$bad_agents = ['sqlmap','nikto','nmap','masscan','acunetix'];
$is_suspicious = false;
$ban_reason = '';
$clean_path = strtok($path, '?');
foreach ($suspicious_exact as $exact) {
if ($clean_path === $exact) {
$is_suspicious = true;
$ban_reason = 'EXACT:'.$exact;
break;
}
}
if (!$is_suspicious) {
foreach ($suspicious_patterns as $pattern) {
if (strpos($uri, $pattern) !== false) {
$is_suspicious = true;
$ban_reason = 'PATTERN:'.$pattern;
break;
}
}
}
if (!$is_suspicious) {
foreach ($bad_agents as $agent) {
if (strpos($user_agent, $agent) !== false) {
$is_suspicious = true;
$ban_reason = 'AGENT:'.$agent;
break;
}
}
}
if (!$is_suspicious && preg_match('#^/[a-z0-9]{1,2}\.php$#', $clean_path)) {
$is_suspicious = true;
$ban_reason = 'SHORT_PHP:'.$clean_path;
}
if ($is_suspicious) {
fw_ban_ip($ip, $current_time + SCAN_BAN_TIME, $ban_reason, $uri, $current_time);
fw_block(404, 'Not found', $is_api);
}
// Rate limiting (skip for API)
if (!$is_api) {
$rate_file = RATE_DIR . 'r_' . md5($ip) . '.dat';
$requests = [];
if (file_exists($rate_file)) {
$data = @file_get_contents($rate_file);
$requests = $data ? json_decode($data, true) : [];
if (!is_array($requests)) $requests = [];
$requests = array_values(array_filter($requests, function($t) use ($current_time) {
return ($current_time - $t) < TIME_WINDOW;
}));
}
$requests[] = $current_time;
if (count($requests) > MAX_REQUESTS) {
fw_ban_ip($ip, $current_time + BAN_TIME, 'RATE_LIMIT:'.count($requests).'req/'.TIME_WINDOW.'s', $uri, $current_time);
@unlink($rate_file);
fw_block(429, 'Too many requests', $is_api, BAN_TIME);
}
@file_put_contents($rate_file, json_encode($requests), LOCK_EX);
}
This firewall was developed through real-world experience defending a production website on shared hosting. All patterns and techniques have been tested against actual attack traffic.
Last Updated: February 2026