Skip to content

Firewall

With the Codefy Firewall, every HTTP request is monitored for SQL injection, XSS, RCE and other attacks. If a request is a threat, it is blocked.

Example Config

./config/firewall.php
<?php

declare(strict_types=1);

return [
    'enabled' => true,

    'block' => true,

    'log_payload' => false,

    'alert_min_severity' => 'high',

    'notifiers' => [],

    'ignored_paths' => [
        '/favicon.ico',
        '/robots.txt',
    ],

    'sql_injection' => [],

    'xss' => [],

    // Remote Code Execution
    'rce' => [],

    'file_traversal' => [],

    // Server-Side Request Forgery
    'ssrf' => [],

    'scanner_path_probe' => [],

    'sensitive_file_probe' => [],

    'php_probe' => [],
];

How It Works

  1. A middleware scans every incoming HTTP request.
  2. The request is checked against regex patterns covering SQL injection, XSS, RCE, file traversal, SSRF, and more.
  3. If a threat pattern matches, the request is immediately blocked.

Verification

You can verify that the firewall is working by triggering a threat:

  • Start your app
php codex serve
  • Test A Registered Route

Append a malicious query parameter to any existing route in your app.

Examples

SQL Injection

http://localhost:8080/?q=' UNION SELECT * FROM users--

XSS (Cross-Site Scripting)

http://localhost:8080/?q=<script>alert(1)</script>

Directory Traversal

http://localhost:8080/?file=../../etc/passwd

RCE (Remote Code Execution)

http://localhost:8080/?cmd=system('ls -la')

Logging

If you want to log the threats, you will need to create an implementation of Codefy\Framework\Security\Firewall\ThreatLogger. Let's say that you wanted to log the threats in a database, you will need to do something similar to the following:

Create a Log Table

CREATE TABLE threat_logs (
    threat_id CHAR(36) PRIMARY KEY,
    ip_address VARCHAR(45) NOT NULL,
    method VARCHAR(10) NOT NULL,
    url TEXT NOT NULL,
    user_agent TEXT NULL,
    threat_type VARCHAR(64) NOT NULL,
    severity VARCHAR(20) NOT NULL,
    confidence_score DECIMAL(5,2) NOT NULL DEFAULT 0.00,
    matched_pattern VARCHAR(255) NULL,
    matched_value TEXT NULL,
    request_payload LONGTEXT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Implement ThreatLogger

<?php

declare(strict_types=1);

namespace Application\Service;

use PDO;
use Psr\Http\Message\ServerRequestInterface;
use Qubus\ValueObjects\Identity\Ulid;

final readonly class PdoThreatLogger
{
    public function __construct(
        private PDO $pdo,
        private bool $logPayload = true,
    ) {
    }

    public function log(ServerRequestInterface $request, ThreatMatch $match): void
    {
        $payload = null;

        if ($this->logPayload) {
            $payload = json_encode([
                'query' => $request->getQueryParams(),
                'body' => $request->getParsedBody(),
            ], JSON_THROW_ON_ERROR);
        }

        $stmt = $this->pdo->prepare(
            'INSERT INTO threat_logs (
                threat_id,
                ip_address,
                method,
                url,
                user_agent,
                threat_type,
                severity,
                confidence_score,
                matched_pattern,
                matched_value,
                request_payload,
                created_at
            ) VALUES (
                :threat_id,
                :ip_address,
                :method,
                :url,
                :user_agent,
                :threat_type,
                :severity,
                :confidence_score,
                :matched_pattern,
                :matched_value,
                :request_payload,
                CURRENT_TIMESTAMP
            )'
        );

        $stmt->execute([
            'threat_id' => Ulid::generateAsString(),
            'ip_address' => $this->ip($request),
            'method' => $request->getMethod(),
            'url' => (string) $request->getUri(),
            'user_agent' => $request->getHeaderLine('User-Agent'),
            'threat_type' => $match->type,
            'severity' => $match->severity,
            'confidence_score' => $match->confidence,
            'matched_pattern' => mb_substr($match->pattern, 0, 255),
            'matched_value' => $match->value,
            'request_payload' => $payload,
        ]);
    }

    private function ip(ServerRequestInterface $request): string
    {
        $server = $request->getServerParams();

        return $server['HTTP_CF_CONNECTING_IP']
            ?? $server['HTTP_X_FORWARDED_FOR']
            ?? $server['REMOTE_ADDR']
            ?? '0.0.0.0';
    }
}

Service Provider

If you are using the skeleton app, then update the Application\Provider\FirewallServiceProvider:

<?php

declare(strict_types=1);

namespace Application\Provider;

use Application\Service\PdoThreatLogger;
use Codefy\Framework\Security\Firewall\ThreatLogger;
use Codefy\Framework\Support\CodefyServiceProvider;

class FirewallServiceProvider extends CodefyServiceProvider
{
    public function register(): void
    {
        $this->codefy->singleton(ThreatLogger::class, function() {
            return new PdoThreatLogger($this->codefy->getDbConnection()->pdo)
        });
    }
}

Then make sure to open ./config/firewall.php, and make sure log_payload is set to true.

Notifier

Along with logging, you can also add notifications by writing an implementation of Codefy\Framework\Security\Firewall\ThreatNotifier:

<?php

declare(strict_types=1);

namespace Application\Service;

use Psr\Http\Message\ServerRequestInterface;
use Codefy\Framework\Security\Firewall\ThreatMatch;
use Codefy\Framework\Security\Firewall\ThreatNotifier;

final readonly class SlackThreatNotifier implements ThreatNotifier
{
    public function __construct(
        private string $webhookUrl,
    ) {
    }

    public function notify(ServerRequestInterface $request, ThreatMatch $match): void
    {
        if ($this->webhookUrl === '') {
            return;
        }

        $message = [
            'text' => sprintf(
                "*Firewall blocked request*\nType: `%s`\nSeverity: `%s`\nConfidence: `%s`\nURL: `%s`",
                $match->type,
                $match->severity,
                $match->confidence,
                (string) $request->getUri(),
            ),
        ];

        $context = stream_context_create([
            'http' => [
                'method' => 'POST',
                'header' => "Content-Type: application/json\r\n",
                'content' => json_encode($message, JSON_THROW_ON_ERROR),
                'timeout' => 2,
            ],
        ]);

        @file_get_contents($this->webhookUrl, false, $context);
    }
}

Then you will need to add your new notifier to ./config/firewall.php:

///
use Application\Service\SlackNotifier;

///

'notifiers' => [
    new SlackNotifier(env('FIREWALL_SLACK_WEBHOOK')),
],

///

When a threat occurs, a new notification will be sent to your Slack channel.