Skip to content

trafficinc/fireline

Repository files navigation

Fireline

Fireline is a low-configuration PHP web application firewall request blocker. It is designed to be loaded before an application request and block obvious malicious traffic before the application handles it.

Fireline currently inspects:

  • Client IP address
  • Query string
  • User agent
  • GET, POST, cookie, header, JSON, and selected raw body values
  • SQL injection, XSS, query abuse, and bot patterns

Requirements

  • PHP 7.1 or newer for runtime compatibility
  • PHP 8.1 or newer for the included development tools
  • ext-json
  • Writable storage/logs/fireline.log

Install dependencies with Composer installed globally:

composer install

Install Methods

Fireline should load before the application handles the request. The preferred deployment is:

  • Keep the fireline package outside the public web root when the host allows it.
  • Put only a small bootstrap file in the public web root.
  • Configure PHP with auto_prepend_file, or call Fireline at the top of the application front controller.
  • Keep storage/logs, storage/replay, and storage/metrics writable by the PHP process.

Example layout:

project/
  fireline/
  public/
    fireline.php

Install dependencies with Composer before deploying:

composer install --no-dev --optimize-autoloader

Copy config-webroot/fireline.php into the web root and update its include path if your layout differs.

Shared Hosting

For cPanel, Plesk, and similar shared hosting, upload the package and vendor/ directory after running Composer locally or in the host's terminal.

Example layout:

/home/account/fireline/
/home/account/public_html/fireline.php
/home/account/public_html/index.php

For .user.ini in public_html:

auto_prepend_file = /home/account/public_html/fireline.php

For .htaccess or Apache PHP config:

php_value auto_prepend_file "/home/account/public_html/fireline.php"

Some shared hosts cache .user.ini changes for a few minutes. If requests are not inspected immediately, wait for PHP-FPM to reload the setting or use the host control panel to restart PHP.

If the host does not allow files outside public_html, place the package in a protected directory and block direct web access to it. Keep replay, metrics, and logs out of publicly served paths whenever possible.

VPS Or Cloud VM

On a VM, keep Fireline beside the site code and configure PHP-FPM or Apache to prepend the bootstrap.

Example layout:

/var/www/fireline/
/var/www/example.com/public/fireline.php
/var/www/example.com/public/index.php

Apache virtual host:

php_admin_value auto_prepend_file "/var/www/example.com/public/fireline.php"

PHP-FPM pool:

php_admin_value[auto_prepend_file] = /var/www/example.com/public/fireline.php

Nginx does not set PHP ini values by itself. Use the PHP-FPM pool, a per-directory .user.ini if enabled, or call Fireline from the application's front controller.

Containers

In Docker or other container builds, install Fireline during the image build and write logs, replay files, and metrics to a mounted volume.

COPY fireline /app/fireline
RUN cd /app/fireline && composer install --no-dev --optimize-autoloader
COPY public/fireline.php /app/public/fireline.php

PHP ini:

auto_prepend_file = /app/public/fireline.php

Mount persistent storage if you want logs, metrics, or replay data to survive container replacement:

/app/fireline/storage/logs
/app/fireline/storage/replay
/app/fireline/storage/metrics

Reverse Proxies And Load Balancers

When the app sits behind Cloudflare, an AWS/GCP/Azure load balancer, Nginx, HAProxy, or another trusted proxy, configure trusted_proxies. Fireline ignores X-Forwarded-For unless REMOTE_ADDR is trusted.

'trusted_proxies' => [
    '10.0.0.0/8',
    '172.16.0.0/12',
    '192.168.0.0/16',
],

For public proxy networks such as Cloudflare, use the provider's published IP ranges and keep them updated. Do not trust all forwarded IP headers on an internet-facing origin.

Web Usage

The web-root fireline.php file loads Fireline and runs it:

<?php

include dirname(__DIR__) . '/fireline/index.php';
$waf = new FireLine();
$waf->run();

If Fireline detects a blocked request, it logs the event, sends:

HTTP/1.1 403 Forbidden

and exits before the application continues.

Framework Integration

Laravel Middleware

Create middleware that runs before application controllers:

<?php

namespace App\Http\Middleware;

use Closure;
use Fireline\Engine\ResponseHandler;
use Fireline\Engine\WafEngine;

class FirelineMiddleware
{
    public function handle($request, Closure $next)
    {
        $decision = (new WafEngine(config('fireline', [])))
            ->inspectCurrentRequest();

        if ($decision->shouldBlock()) {
            ResponseHandler::block($decision);
            exit;
        }

        return $next($request);
    }
}

Register it early in the web middleware group or as global middleware. Publish Fireline settings into config/fireline.php if you want Laravel-native config loading.

Symfony Kernel Request Listener

Register a kernel.request listener with a high priority so it runs before controllers:

<?php

namespace App\EventListener;

use Fireline\Engine\WafEngine;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class FirelineRequestListener
{
    /** @var array<string, mixed> */
    private $config;

    public function __construct(array $config = [])
    {
        $this->config = $config;
    }

    public function onKernelRequest(RequestEvent $event): void
    {
        if (method_exists($event, 'isMainRequest') && !$event->isMainRequest()) {
            return;
        }

        $decision = (new WafEngine($this->config))->inspectCurrentRequest();

        if ($decision->shouldBlock()) {
            $event->setResponse(new Response('Blocked', 403));
        }
    }
}

config/services.yaml:

services:
  App\EventListener\FirelineRequestListener:
    arguments:
      $config: '%fireline.config%'
    tags:
      - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 512 }

For the earliest possible coverage, auto_prepend_file can still be used in front of Symfony.

WordPress Bootstrap Or Plugin

The earliest WordPress protection is still auto_prepend_file, because it runs before WordPress loads. For easier management, create a small plugin:

<?php
/**
 * Plugin Name: Fireline WAF
 */

require_once WP_CONTENT_DIR . '/../fireline/index.php';

add_action('plugins_loaded', static function (): void {
    $waf = new FireLine();
    $waf->run();
}, 0);

Place it at:

wp-content/plugins/fireline-waf/fireline-waf.php

Activate it in WordPress. Use auto_prepend_file instead when you need Fireline to inspect requests before any WordPress bootstrap code runs.

Generic Front-Controller Apps

For Slim, custom MVC apps, and other front-controller projects, call Fireline at the top of public/index.php before the application container or router is started:

<?php

require dirname(__DIR__, 2) . '/fireline/index.php';

$waf = new FireLine();
$waf->run();

require dirname(__DIR__) . '/app/bootstrap.php';

$app->run();

This is less automatic than auto_prepend_file, but it works when the host does not allow PHP ini changes.

Production Checklist

Before enabling Fireline on production traffic:

  • Run composer install --no-dev --optimize-autoloader.
  • Copy config.php.example to config.php only when defaults need to be changed.
  • Verify storage/logs, storage/replay, and storage/metrics permissions for the PHP user.
  • Run php fire.php config:check from the Fireline directory.
  • Start with paranoia_level set to medium, or low for applications with very low false-positive tolerance.
  • Configure trusted_proxies before relying on forwarded client IP headers.
  • Enable replay_enabled during tuning, then protect or rotate replay files because they contain normalized request data.
  • Set metrics_path when you want cross-request tuning data.
  • Test one benign request and one obvious blocked request, such as ?q=javascript:alert(1), after deployment.

Configuration

Fireline works without a config file. To override defaults, copy config.php.example to config.php in the Fireline directory.

<?php

return [
    'bypass_firewall' => false,
    'strict_mode' => false,
    'ip_by_country' => false,
    'whitelist' => false,
    'trusted_proxies' => [],
    'max_fields' => 200,
    'max_headers' => 100,
    'max_header_length' => 8192,
    'max_body_length' => 1048576,
    'max_value_length' => 8192,
    'inspect_json' => true,
    'inspect_headers' => true,
    'inspect_raw_body' => true,
    'metrics_path' => null,
    'score_threshold' => null,
    'regex_threshold' => null,
    'safe_cache_threshold' => null,
];

Config options:

  • bypass_firewall: disables all filtering when set to true.
  • strict_mode: normalizes query strings before query filtering.
  • ip_by_country: enables country blocking using src/GeoLite2-Country.mmdb and src/Compares/ip_block_by_country.php.
  • whitelist: enables IP whitelist mode using src/Compares/ips_white_list.php. When enabled, IP blacklist mode is not used.
  • trusted_proxies: proxy IPs or CIDR ranges allowed to supply X-Forwarded-For.
  • max_fields: maximum extracted fields before the request is blocked.
  • max_headers: maximum HTTP headers before the request is blocked.
  • max_header_length: maximum bytes allowed for an individual header value.
  • max_body_length: maximum raw request body bytes before the request is blocked.
  • max_value_length: maximum characters inspected per request value.
  • inspect_json: inspects JSON request bodies for application/json requests.
  • inspect_headers: inspects HTTP headers, excluding Cookie because cookies are inspected separately.
  • inspect_raw_body: inspects raw bodies for non-form and non-JSON content types.
  • Multipart uploads are inspected through metadata fields such as filename, client MIME type, size, and upload error. Fireline does not read uploaded file contents or log temporary upload paths.
  • paranoia_level: detection posture. Supported values are low, medium, high, and strict.
  • replay_enabled: writes normalized replay events when set to true.
  • replay_path: JSON-lines replay file path.
  • metrics_path: optional JSON file path for persisted aggregate metrics.
  • score_threshold: field score required to block a request.
  • regex_threshold: score required before conditional regex rules run.
  • safe_cache_threshold: maximum score eligible for short-lived safe fingerprint caching.

Architecture

The current engine follows a staged inspection pipeline:

  1. Extract request fields individually.
  2. Reject requests that exceed configured limits or contain malformed encoding.
  3. Normalize each field once.
  4. Build a route/field/shape fingerprint.
  5. Check short-lived safe and threat caches.
  6. Run cheap prefilters and heuristics.
  7. Run keyword scanning.
  8. Run regex rules only after suspicious signals.
  9. Score and decide.
  10. Log and block, or allow the application to continue.

The public FireLine class remains available for existing integrations, but internally delegates to Fireline\Engine\WafEngine.

Legacy Compatibility

The historical Filters\* and Handlers\* classes remain available for older integrations. They are compatibility wrappers; new integrations should use FireLine or Fireline\Engine\WafEngine. Compatibility filters for SQL, XSS, query, bot, and IP checks now delegate to the staged engine or guard classes so behavior stays aligned with the current request pipeline.

Trusted proxy example:

'trusted_proxies' => [
    '127.0.0.1',
    '10.0.0.0/8',
],

Leave trusted_proxies empty unless the site is behind a reverse proxy or load balancer you control.

Route Models

Optional route models live in config/routes.php. They add anomaly score when a known route receives a field shape that does not match the expected type or length.

return [
    '/login' => [
        'post.username' => [
            'type' => 'alnum',
            'max_length' => 64,
            'allowed_chars' => 'alnum',
            'denied_tokens' => ['union', 'select', 'sleep', 'script'],
        ],
        'post.password' => [
            'type' => 'opaque',
            'max_length' => 256,
        ],
        'get.q' => [
            'type' => 'text',
            'max_length' => 256,
            'allowed_chars' => 'free_text',
        ],
    ],
];

Supported field types are alpha, alnum, int, integer, numeric, email, slug, url, text, and opaque.

Route fields can define:

  • min_length, max_length, and avg_length
  • allowed_chars: alpha, alnum, slug, free_text, or a bounded regex
  • shape: a normalized shape from ShapeModel::shape()
  • required_tokens: tokens expected to appear
  • denied_tokens: route-specific tokens that should raise anomaly score

Route models are scoring signals, not standalone block rules.

Paranoia Levels

Paranoia levels provide adoption-friendly defaults:

  • low: conservative blocking for high false-positive sensitivity.
  • medium: default balanced mode.
  • high: more aggressive scoring and earlier regex checks.
  • strict: aggressive mode for applications that can tolerate more blocking.

Explicit score_threshold, regex_threshold, and safe_cache_threshold values override the level defaults.

Rules also declare a paranoia level. Fireline only runs rules at or below the configured level, so low mode uses the highest-confidence rules while strict mode includes every rule.

Explainability

Every decision can produce a developer-facing explanation:

$decision = $waf->inspectCurrentRequest();

echo $decision->explain(25);

Example:

Blocked:
- rule:SQL_BOOLEAN_OPERATOR (+6)
- encoding_heuristics (+4)
- route_model (+7)
Final Score: 17
Threshold: 15

Use $decision->explanation() when structured data is easier to display or store.

Replay

Replay mode stores normalized request fields, matched rules, scores, and decisions as JSON lines. Sensitive fields such as passwords, tokens, API keys, secrets, and authorization values are redacted before writing replay data. Enable it in config:

'replay_enabled' => true,
'replay_path' => __DIR__ . '/storage/replay/traffic.ndjson',

Replay stored traffic after rule or scoring changes:

use Fireline\Replay\ReplayRunner;

$result = (new ReplayRunner())->replay(__DIR__ . '/storage/replay/traffic.ndjson');

foreach ($result['regressions'] as $regression) {
    print_r($regression);
}

Replay uses the stored normalized fields and re-scores them with the current engine, which helps catch new blocks, missed blocks, score increases, and false-positive regressions before deployment. Replay metadata includes thresholds, paranoia level, selected config values, and an active rule-set fingerprint so config changes and rule changes can be distinguished. When metadata differs, replay output reports the changed metadata groups, such as thresholds, config, or rules. Invalid replay lines are counted separately so corrupt capture files are visible. Replay summaries also include decision-change counts and score-delta aggregates, so broad tuning drift is visible even when traffic does not cross the block threshold.

The same replay check is available from the CLI:

php fire.php replay:run storage/replay/traffic.ndjson

Use --ci to return a non-zero exit code when replay regressions are found:

php fire.php replay:run storage/replay/traffic.ndjson --ci

Use --json when automation needs the full replay result:

php fire.php replay:run storage/replay/traffic.ndjson --json

Write a JSON replay report for CI artifacts or rule-review notes:

php fire.php replay:run storage/replay/traffic.ndjson --output storage/replay/report.json
php fire.php replay:run storage/replay/traffic.ndjson --output storage/replay/report.json --force

Existing report files are not overwritten unless --force is supplied.

Build route model candidates from replay data:

php fire.php baseline:build storage/replay/traffic.ndjson 10
php fire.php baseline:build storage/replay/traffic.ndjson 10 --json
php fire.php baseline:build storage/replay/traffic.ndjson 10 --json --report
php fire.php baseline:export storage/replay/traffic.ndjson 10 storage/models/routes.generated.php
php fire.php baseline:export storage/replay/traffic.ndjson 10 storage/models/routes.generated.php --dry-run
php fire.php baseline:export storage/replay/traffic.ndjson 10 storage/models/routes.generated.php --force

baseline:build prints a PHP config/routes.php fragment for review by default. Use --json when automation needs the candidate model directly, or --json --report when it also needs replay read counts and invalid-line counts. Use baseline:export to write the reviewed candidate model to a target PHP file. Add --dry-run to preview the destination and replay counts without writing. Existing files are not overwritten unless --force is supplied.

Validate configuration, writable paths, and rule metadata:

php fire.php config:check

Rule Files

The staged WAF rule set is stored in config/rules.php. Each rule includes:

  • id: stable rule identifier used in decisions, replay, and metrics.
  • type: keyword for Aho-Corasick scanning or regex for conditional regex confirmation.
  • pattern: keyword text or a bounded regular expression.
  • score: contribution to the field score.
  • category: detection family such as sqli, xss, lfi, rfi, webshell, scanner, php_injection, protocol, or upload.
  • paranoia: minimum posture where the rule is active: low, medium, high, or strict.
  • explanation, examples, and false_positives: reviewer context for tuning.

Keyword rules run first through the Aho-Corasick scanner. Regex rules run only after the field is already suspicious and their required tokens are present.

Validate rule metadata and regex syntax after editing the rule set:

php fire.php rules:validate
php fire.php rules:validate config/rules.php --json

Legacy compare lists remain in src/Compares for compatibility wrappers and guard lists:

  • bots.php: blocked user agents used by BotGuard.
  • ips.php: blocked IPs or partial IP strings used by IpGuard.
  • ips_white_list.php: allowed IPs and CIDR ranges when whitelist mode is enabled.
  • ip_block_by_country.php: country ISO codes blocked when country blocking is enabled.

The historical SQL, XSS, and query compare files have been removed. Compatibility filters now delegate detection to the staged engine, so new rule work should happen in config/rules.php.

Logging

Blocked requests are logged to:

storage/logs/fireline.log

Logs are written as JSON lines. Each blocked request is one JSON object:

{"level":"warn","event":"fireline.blocked_request","timestamp":"2026-05-13T12:00:00-04:00","unix_time":1778688000,"remote_addr":"203.0.113.10","method":"GET","route":"/products","request_uri":"/products?id=1","filter":"get","field":"get.id","score":30,"matched_score":30,"reason":"field_score_threshold","value":"1 union select password from users","normalized":"1 union select password from users","user_agent":"Mozilla/5.0","referer":"https://example.com/"}

Event fields:

  • level: always warn for blocked requests.
  • event: always fireline.blocked_request.
  • timestamp: ISO-8601 timestamp.
  • unix_time: Unix timestamp.
  • remote_addr: REMOTE_ADDR from PHP.
  • method: HTTP request method.
  • route: parsed request path.
  • request_uri: request URI from PHP.
  • filter: field source that blocked the request, such as get, post, cookie, header, json, raw, ip, or bot.
  • field: exact inspected field that crossed the threshold.
  • score: total decision score.
  • matched_score: score for the exact blocking field.
  • reason: decision reason.
  • value: matched value after sanitization and redaction.
  • normalized: normalized matched value after sanitization and redaction.
  • user_agent: user agent from PHP.
  • referer: referer from PHP.

Attacker-controlled fields are sanitized before logging:

  • Control characters are replaced with spaces.
  • Common secret parameters such as password, token, api_key, secret, and authorization are redacted.
  • Logged values are capped at 1000 characters.

If the log directory or file does not exist, Fireline attempts to create it. If the log file is not writable, Fireline throws an exception. Make sure storage/logs is writable by the PHP process.

Profiling And Metrics

Fireline records lightweight in-process metrics for tuning rules and cache behavior.

use Fireline\Telemetry\RuleMetrics;

$snapshot = RuleMetrics::snapshot();

The snapshot includes:

  • counters: rule execution counts, rule match counts, false-positive counts, and cache writes.
  • timings: scanner and regex timing data with count, total_ms, and max_ms.
  • cache_hit_ratios: safe/threat cache hit ratios.
  • slowest_rules: timing data sorted by slowest maximum execution time.

Examples:

RuleMetrics::increment('rule.SQL_UNION_SELECT.executed');
RuleMetrics::timing('rule.SQL_UNION_SELECT', 0.14);
RuleMetrics::falsePositive('SQL_UNION_SELECT');

Current instrumentation tracks:

  • Keyword scanner timing
  • Keyword rule match counts
  • Regex rule execution counts
  • Regex rule match counts
  • Regex rule timings
  • Request limit evaluations and violations
  • Safe/threat cache hits and misses
  • Safe/threat cache writes
  • Manual false-positive counters

To persist metrics across web requests, set metrics_path:

'metrics_path' => __DIR__ . '/storage/metrics/fireline-metrics.json',

The metrics file stores an aggregate snapshot. Each inspected request contributes its current metrics delta, and malformed or partially written metrics files are treated as empty snapshots on the next write.

Then inspect the persisted aggregate from the CLI:

php fire.php metrics:show storage/metrics/fireline-metrics.json
php fire.php metrics:show storage/metrics/fireline-metrics.json --json
php fire.php metrics:export storage/metrics/fireline-metrics.json storage/metrics/export.json

CLI And Development Commands

Run tests:

composer test

Run the smoke test:

composer run smoke

Run PHP syntax checks:

composer run lint

Validate rule metadata and regex syntax:

composer run rules:validate

Validate configuration, writable paths, and rules:

composer run config:check

Run the full local verification suite:

composer run check

The fire.php CLI exposes help, replay:run, baseline:build, baseline:export, config:check, rules:validate, metrics:show, metrics:export, and metrics:reset.

Show the current in-process metrics snapshot:

php fire.php metrics:show
php fire.php metrics:show --json
php fire.php metrics:show storage/metrics/fireline-metrics.json --summary
php fire.php metrics:export storage/metrics/fireline-metrics.json storage/metrics/export.json
php fire.php metrics:reset storage/metrics/fireline-metrics.json

Troubleshooting

Requests are not being blocked

  • Confirm auto_prepend_file points to the copied web-root fireline.php.
  • Confirm PHP loaded the setting with phpinfo() or the host control panel.
  • Confirm the web-root fireline.php includes the correct path to fireline/index.php.
  • Confirm bypass_firewall is not set to true.
  • Add a temporary test query such as ?q=javascript:alert(1) and verify a 403 Forbidden response.

The site returns a PHP error after enabling Fireline

  • Confirm Composer dependencies were installed with composer install --no-dev --optimize-autoloader.
  • Confirm fireline.php uses an absolute include path that matches the deployed directory layout.
  • Confirm the PHP runtime version is PHP 7.1 or newer.

All traffic appears to come from the proxy

Set trusted_proxies to the IP or CIDR range of your reverse proxy. Fireline ignores X-Forwarded-For unless REMOTE_ADDR is trusted.

Legitimate traffic is blocked

  • Lower paranoia_level to low or raise score_threshold while reviewing the event.
  • Check storage/logs/fireline.log for the matched field, score, reason, and normalized value.
  • Enable replay temporarily, reproduce the request, and replay it after rule or route-model changes.
  • Review route models for fields that intentionally allow free text, URLs, code snippets, or search syntax.

Country blocking blocks unexpectedly

Country blocking fails closed if enabled and the GeoIP database is missing or unreadable. Confirm src/GeoLite2-Country.mmdb exists and is readable.

Logs are not written

  • Confirm storage/logs/fireline.log exists.
  • Confirm it is writable by the web server user.
  • Confirm PHP has permission to write inside storage/logs.

Metrics or replay files are not written

  • Confirm metrics_path or replay_path points to a writable directory.
  • Confirm replay is enabled with replay_enabled => true.
  • Keep these files outside public web access because they may contain normalized request data.

About

Fireline: Web Application Firewall (WAF) for PHP

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages