senza1dio / security-shield
Security Middleware for PHP - Honeypot, Scanner Detection, Rate Limiting, Resilience Patterns
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 3
Watchers: 0
Forks: 2
Open Issues: 0
pkg:composer/senza1dio/security-shield
Requires
- php: ^8.1
- ext-json: *
Requires (Dev)
- ext-pdo: *
- ext-redis: *
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^1.10
- phpunit/phpunit: >=10.1 <12.0
This package is auto-updated.
Last update: 2026-01-25 10:48:02 UTC
README
Security Middleware for PHP 8.1+
Honeypot, scanner detection, and resilience patterns for PHP applications.
What This Package Does
- Honeypot System - 69 trap endpoints to catch scanners (/.env, /wp-admin, etc.)
- Scanner Detection - Identifies sqlmap, nikto, masscan by signatures
- Rate Limiting - 4 algorithms: sliding window, token bucket, leaky bucket, fixed window
- IP Scoring - Accumulates threat scores based on behavior
- Bot Verification - DNS-based verification for Googlebot, Bingbot
- Geo-Blocking - Country-level restrictions via external GeoIP provider
What This Package Does NOT Do
- Not a WAF - No SQL injection, XSS, or OWASP Top 10 detection
- Not DDoS Protection - Cannot handle volumetric attacks (use Cloudflare/AWS Shield)
- Not ML-Based - No machine learning, just signature and statistical detection
- Not Penetration Tested - Has not undergone professional security audit
Use alongside a real WAF (ModSecurity, Cloudflare) for production.
Architecture
Resilience Patterns
| Pattern | Description | Storage Required |
|---|---|---|
| Circuit Breaker | Fail fast when dependency is down | Redis (distributed) or none (local) |
| Retry Policy | Exponential backoff with jitter | None |
| Fallback Chain | Try providers in order until success | None |
| Bulkhead | Limit concurrent executions | Redis |
Observability
| Component | Format | Notes |
|---|---|---|
| Tracing | OpenTelemetry-compatible | W3C traceparent context propagation |
| Metrics | Prometheus text format | Counters, gauges, histograms |
| Health | JSON + HTTP status | Liveness/readiness for Kubernetes |
Anomaly Detection
| Detector | What It Detects |
|---|---|
| Statistical | Values outside Z-score threshold |
| Rate | Request rate spikes/drops |
| Pattern | Unusual paths, methods, user agents |
| Time-Based | Activity during unusual hours |
Installation
composer require senza1dio/security-shield
Quick Start
Option 1: No Dependencies (Development/Testing)
<?php use Senza1dio\SecurityShield\Middleware\SecurityMiddleware; use Senza1dio\SecurityShield\Config\SecurityConfig; use Senza1dio\SecurityShield\Storage\NullStorage; // In-memory storage - NO Redis/Database required $config = (new SecurityConfig()) ->setStorage(new NullStorage()); $security = new SecurityMiddleware($config); if (!$security->handle($_SERVER)) { http_response_code(403); exit('Access Denied'); }
Note: NullStorage loses data between requests. Use for testing only.
Option 2: Database Storage (Production without Redis)
<?php use Senza1dio\SecurityShield\Storage\DatabaseStorage; // Use your existing database - NO Redis required $pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'); $config = (new SecurityConfig()) ->setStorage(new DatabaseStorage($pdo)); $security = new SecurityMiddleware($config); if (!$security->handle($_SERVER)) { http_response_code(403); exit('Access Denied'); }
Option 3: Redis Storage (Recommended for Production)
<?php use Senza1dio\SecurityShield\Storage\RedisStorage; // Fastest option - requires ext-redis $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $config = (new SecurityConfig()) ->setStorage(new RedisStorage($redis)); $security = new SecurityMiddleware($config); if (!$security->handle($_SERVER)) { http_response_code(403); exit('Access Denied'); }
Usage Examples
Circuit Breaker
use Senza1dio\SecurityShield\Resilience\CircuitBreaker; $breaker = new CircuitBreaker('redis', $storage, [ 'failure_threshold' => 5, // Open after 5 failures 'recovery_timeout' => 30, // Try again after 30s 'half_open_max_calls' => 3, // Allow 3 test calls ]); // State logged to error_log on transitions $result = $breaker->call( fn() => $redis->get('key'), fn() => 'fallback-value' );
Limitation: In PHP-FPM, each worker has independent in-memory state if Redis unavailable.
Retry Policy
use Senza1dio\SecurityShield\Resilience\RetryPolicy; $policy = RetryPolicy::exponentialBackoffWithJitter( maxAttempts: 5, baseDelay: 1.0, maxDelay: 30.0 ); // Delays: ~1s, ~2s, ~4s, ~8s (with random jitter) $result = $policy->execute(fn() => $api->call());
Rate Limiting
use Senza1dio\SecurityShield\RateLimiting\RateLimiter; // Token bucket: 100 tokens, refills 10/second $limiter = RateLimiter::tokenBucket($storage, 100, 10); $result = $limiter->attempt('user:123'); if (!$result->allowed) { // $result->retryAfter contains seconds to wait http_response_code(429); exit; }
Health Checks
use Senza1dio\SecurityShield\Health\HealthCheck; use Senza1dio\SecurityShield\Health\Checks\RedisHealthCheck; $health = new HealthCheck(); $health->addCheck('redis', new RedisHealthCheck($redis)); // Returns HealthResult with HTTP status code $result = $health->readiness(); header('Content-Type: application/json'); http_response_code($result->getHttpStatusCode()); echo $result->toJson();
Distributed Tracing
use Senza1dio\SecurityShield\Telemetry\Tracer; use Senza1dio\SecurityShield\Telemetry\SpanKind; $tracer = new Tracer('my-service', '1.0.0'); // Extract parent context from incoming request $parentContext = $tracer->extractContext(getallheaders()); $span = $tracer->startSpanFromContext('handle-request', $parentContext, SpanKind::SERVER); $span->setAttribute('http.method', $_SERVER['REQUEST_METHOD']); // ... process request ... $span->setStatus(SpanStatus::OK); $tracer->endSpan($span); $tracer->flush(); // Export spans
Hot-Reload Configuration
use Senza1dio\SecurityShield\Config\ConfigProvider; $config = new ConfigProvider($storage, [ 'cache_ttl' => 60, // Reload from Redis every 60s ]); $config->setDefaults(['threshold' => 50]); // Update from anywhere - all instances pick up changes $config->setRemote('threshold', 100); // Later reads get new value after cache expires $value = $config->get('threshold'); // 100
Note: Changes propagate on cache expiry, not instantly.
Notifications
use Senza1dio\SecurityShield\Notifications\NotificationManager; use Senza1dio\SecurityShield\Notifications\TelegramNotifier; use Senza1dio\SecurityShield\Notifications\SlackNotifier; $manager = new NotificationManager(); $manager->addChannel(new TelegramNotifier($botToken, $chatId)); $manager->addChannel(new SlackNotifier($webhookUrl)); // Send to all channels $result = $manager->broadcast('Security Alert', 'IP banned: 1.2.3.4', [ 'reason' => 'Honeypot access', ]); // Check results if (!$result->allSuccessful()) { foreach ($result->getErrors() as $channel => $error) { error_log("Notification to {$channel} failed: {$error}"); } }
Configuration Validation
use Senza1dio\SecurityShield\Config\ConfigValidator; $validator = ConfigValidator::create() ->required() ->type('integer') ->min(1) ->max(1000); $result = $validator->validate($value); if (!$result->valid) { throw new InvalidArgumentException($result->error); }
Requirements
- PHP 8.1+ (uses enums, readonly properties)
- ext-json
Optional Extensions
- ext-redis (for RedisStorage - recommended for production)
- ext-pdo (for DatabaseStorage)
- ext-curl (for notification channels, GeoIP)
Storage Backends
Choose the right storage for your use case:
| Backend | Use Case | Dependencies | Performance | Persistence |
|---|---|---|---|---|
| NullStorage | Testing, Development | ✅ None | ~0.001ms | ❌ No (in-memory) |
| DatabaseStorage | Production (no Redis) | ext-pdo |
~1-5ms | ✅ Yes (MySQL/PostgreSQL) |
| RedisStorage | Production (recommended) | ext-redis |
~0.05ms | ✅ Yes (distributed) |
When to Use Each Backend
NullStorage - Development/Testing Only
$config = (new SecurityConfig())->setStorage(new NullStorage());
- ✅ Zero setup, no dependencies
- ✅ Perfect for unit tests
- ❌ Data lost between requests
- ❌ NOT for production
DatabaseStorage - Production without Redis
$pdo = new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'); $config = (new SecurityConfig())->setStorage(new DatabaseStorage($pdo));
- ✅ No extra infrastructure needed
- ✅ Uses existing database
- ✅ Persistent across servers
- ⚠️ Slower than Redis (1-5ms vs 0.05ms)
RedisStorage - Production (Best Performance)
$redis = new Redis(); $redis->connect('127.0.0.1', 6379); $config = (new SecurityConfig())->setStorage(new RedisStorage($redis));
- ✅ Ultra-fast (~0.05ms)
- ✅ Distributed state across servers
- ✅ Built-in TTL expiration
- ⚠️ Requires Redis server
Known Limitations
- No Persistence in NullStorage - Data lost between requests
- Clock Skew - Rate limiting assumes synchronized clocks
- Memory Growth - Tracer spans queue in memory until flush
- Blocking Operations - SMTP notifications block during send
- No Clustering - Each PHP worker has independent memory state
Error Handling
All network operations log errors to error_log():
- SMTP failures
- Webhook failures
- Redis connection issues
- Circuit breaker state changes
Configure PHP error_log to capture these in production.
Testing
composer install composer test # PHPUnit tests composer stan # PHPStan level 8 composer cs-check # Code style check
License
MIT License - see LICENSE