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
- 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 installFireline should load before the application handles the request. The preferred deployment is:
- Keep the
firelinepackage 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, andstorage/metricswritable by the PHP process.
Example layout:
project/
fireline/
public/
fireline.php
Install dependencies with Composer before deploying:
composer install --no-dev --optimize-autoloaderCopy config-webroot/fireline.php into the web root and update its include path if your layout differs.
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.phpFor .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.
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.phpNginx 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.
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.phpPHP ini:
auto_prepend_file = /app/public/fireline.phpMount 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
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.
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.
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.
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.
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.
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.
Before enabling Fireline on production traffic:
- Run
composer install --no-dev --optimize-autoloader. - Copy
config.php.exampletoconfig.phponly when defaults need to be changed. - Verify
storage/logs,storage/replay, andstorage/metricspermissions for the PHP user. - Run
php fire.php config:checkfrom the Fireline directory. - Start with
paranoia_levelset tomedium, orlowfor applications with very low false-positive tolerance. - Configure
trusted_proxiesbefore relying on forwarded client IP headers. - Enable
replay_enabledduring tuning, then protect or rotate replay files because they contain normalized request data. - Set
metrics_pathwhen you want cross-request tuning data. - Test one benign request and one obvious blocked request, such as
?q=javascript:alert(1), after deployment.
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 totrue.strict_mode: normalizes query strings before query filtering.ip_by_country: enables country blocking usingsrc/GeoLite2-Country.mmdband 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 supplyX-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 forapplication/jsonrequests.inspect_headers: inspects HTTP headers, excludingCookiebecause 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 arelow,medium,high, andstrict.replay_enabled: writes normalized replay events when set totrue.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.
The current engine follows a staged inspection pipeline:
- Extract request fields individually.
- Reject requests that exceed configured limits or contain malformed encoding.
- Normalize each field once.
- Build a route/field/shape fingerprint.
- Check short-lived safe and threat caches.
- Run cheap prefilters and heuristics.
- Run keyword scanning.
- Run regex rules only after suspicious signals.
- Score and decide.
- 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.
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.
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, andavg_lengthallowed_chars:alpha,alnum,slug,free_text, or a bounded regexshape: a normalized shape fromShapeModel::shape()required_tokens: tokens expected to appeardenied_tokens: route-specific tokens that should raise anomaly score
Route models are scoring signals, not standalone block rules.
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.
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 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.ndjsonUse --ci to return a non-zero exit code when replay regressions are found:
php fire.php replay:run storage/replay/traffic.ndjson --ciUse --json when automation needs the full replay result:
php fire.php replay:run storage/replay/traffic.ndjson --jsonWrite 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 --forceExisting 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 --forcebaseline: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:checkThe staged WAF rule set is stored in config/rules.php. Each rule includes:
id: stable rule identifier used in decisions, replay, and metrics.type:keywordfor Aho-Corasick scanning orregexfor conditional regex confirmation.pattern: keyword text or a bounded regular expression.score: contribution to the field score.category: detection family such assqli,xss,lfi,rfi,webshell,scanner,php_injection,protocol, orupload.paranoia: minimum posture where the rule is active:low,medium,high, orstrict.explanation,examples, andfalse_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 --jsonLegacy compare lists remain in src/Compares for compatibility wrappers and guard lists:
bots.php: blocked user agents used byBotGuard.ips.php: blocked IPs or partial IP strings used byIpGuard.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.
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: alwayswarnfor blocked requests.event: alwaysfireline.blocked_request.timestamp: ISO-8601 timestamp.unix_time: Unix timestamp.remote_addr:REMOTE_ADDRfrom PHP.method: HTTP request method.route: parsed request path.request_uri: request URI from PHP.filter: field source that blocked the request, such asget,post,cookie,header,json,raw,ip, orbot.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, andauthorizationare 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.
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 withcount,total_ms, andmax_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.jsonRun tests:
composer testRun the smoke test:
composer run smokeRun PHP syntax checks:
composer run lintValidate rule metadata and regex syntax:
composer run rules:validateValidate configuration, writable paths, and rules:
composer run config:checkRun the full local verification suite:
composer run checkThe 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- Confirm
auto_prepend_filepoints to the copied web-rootfireline.php. - Confirm PHP loaded the setting with
phpinfo()or the host control panel. - Confirm the web-root
fireline.phpincludes the correct path tofireline/index.php. - Confirm
bypass_firewallis not set totrue. - Add a temporary test query such as
?q=javascript:alert(1)and verify a403 Forbiddenresponse.
- Confirm Composer dependencies were installed with
composer install --no-dev --optimize-autoloader. - Confirm
fireline.phpuses an absolute include path that matches the deployed directory layout. - Confirm the PHP runtime version is PHP 7.1 or newer.
Set trusted_proxies to the IP or CIDR range of your reverse proxy. Fireline ignores X-Forwarded-For unless REMOTE_ADDR is trusted.
- Lower
paranoia_leveltolowor raisescore_thresholdwhile reviewing the event. - Check
storage/logs/fireline.logfor 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 fails closed if enabled and the GeoIP database is missing or unreadable. Confirm src/GeoLite2-Country.mmdb exists and is readable.
- Confirm
storage/logs/fireline.logexists. - Confirm it is writable by the web server user.
- Confirm PHP has permission to write inside
storage/logs.
- Confirm
metrics_pathorreplay_pathpoints 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.