Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebDecoy PHP Sensor

Report bot and AI-crawler traffic to WebDecoy from any PHP application.

PHP runs roughly 70% of the server-side web, and WordPress is about 58% of that. Everything else (Laravel, Symfony, Magento, Drupal, bespoke PHP) had no way to report a detection at all. This is that way.

It is a sensor, not a firewall. It observes and reports. It never blocks, never challenges, never modifies a response. Enforcement is a separate decision made elsewhere, with the evidence this produces.


Install

composer require webdecoy/sensor

Use it

Pick the narrowest integration your stack allows. In order of preference:

A framework middleware (preferred)

You have a request object, so use it. Nothing global, nothing to remember.

use WebDecoy\Sensor\{Sensor, Config, Request};

$sensor = new Sensor(new Config(
    apiKey: getenv('WEBDECOY_API_KEY'),
    source: 'laravel_middleware',
    trustedProxies: ['10.0.0.0/8'],   // see below, this one matters
));

// Laravel: in a terminable middleware, so reporting happens after the
// response has already been sent to the visitor.
public function terminate($request, $response): void
{
    $this->sensor->observe(new Request(
        $request->method(),
        $request->fullUrl(),
        $request->server('REMOTE_ADDR'),
        array_change_key_case($request->headers->all(), CASE_LOWER)
    ));
}

auto_prepend_file (covers everything, bigger blast radius)

For a host where you want coverage without touching application code.

; php.ini
auto_prepend_file = /path/to/vendor/webdecoy/sensor/bootstrap.php
# .htaccess, which LiteSpeed also honours
php_value auto_prepend_file /path/to/vendor/webdecoy/sensor/bootstrap.php

Configure it from the environment:

WEBDECOY_API_KEY=sk_live_...
WEBDECOY_TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12
WEBDECOY_SOURCE=php_sensor

Understand what this switches on. A prepended file runs for every PHP process on the host: web requests, cron jobs, queue workers, composer install. The sensor skips the CLI by default for that reason, but the blast radius is the whole machine rather than one application. Prefer a middleware where you have one.


Turning it off

Two ways, because an operator whose site is misbehaving needs it off now, without a deploy and without waiting for us.

WEBDECOY_DISABLE=1            # if you can restart PHP
# if you cannot: point at a path, then create the file
WEBDECOY_DISABLE_FILE=/var/run/webdecoy.disabled
touch /var/run/webdecoy.disabled

Either one stops the sensor before it does anything else.


Trusted proxies: the setting to get right

Default is empty, which means the socket address is used and forwarding headers are ignored. That is correct for a server with nothing in front of it and wrong for one behind a CDN or load balancer.

If you are behind Cloudflare, a load balancer, or any reverse proxy, list it:

trustedProxies: ['10.0.0.0/8', '172.16.0.0/12']

Get this wrong and nothing errors. Every visitor collapses onto one address, the address of your proxy, which silently defeats rate limiting and makes actor correlation meaningless. It looks exactly like normal operation.

The sensor walks X-Forwarded-For from the right, skipping hops you have declared trusted, and stops at the first one you have not. The leftmost entry is whatever the client claimed, so it is never believed on its own.


Tripwire paths

Nobody browses to /.env by accident. A request for one is not suspicious, it is conclusive: the caller is enumerating for credentials, and no JavaScript, fingerprint, or reputation lookup was needed to know it. That makes a trap hit the most valuable thing a server-side sensor can observe, and the cheapest, since it is a string comparison on the path.

Eighteen paths are armed by default (credential files, source-control metadata, database dumps, debug endpoints). Add your own:

tripwirePaths: ['/admin-backup', '/old-wp-config.txt']
WEBDECOY_TRIPWIRE_PATHS=/admin-backup,/old-wp-config.txt

Matching a tripwire changes nothing about the response. The sensor does not block, redirect, or serve anything different. It reports, and reports honestly that a trap was reached. What happens next is a decision made elsewhere, with more context than a single request has.

Matching is an exact comparison on the normalized path (lowercased, query and fragment removed, trailing slash trimmed). Deliberately not a prefix or substring match: a legitimate page whose URL merely contains one of these strings would otherwise register as a trap hit, and a false positive on the heaviest signal we have costs more than a missed scan.

Trap hits are checked before the static-asset skip, because several bait paths end in an extension that skip list would otherwise discard (/backup.zip, /database.sql, /.DS_Store).

The same eighteen paths ship in the Node SDK and the WordPress plugin, so one request is a trap hit on every stack or on none.


What it sends

The full wire contract is public: https://docs.webdecoy.com/reference/sensor-protocol/

Request metadata only: user agent, resolved client IP, URL, method, and the client's header names plus Accept-Language and Accept-Encoding.

It does not send request or response bodies, cookie values, or credentials. Header names, not values, because the name set is what carries the signal and your visitors' header values are not ours to collect.


Testing your install

curl -A "WebDecoy-Test/1.0" https://your-site.example/

A detection appears in your dashboard, labelled as a test and excluded from statistics, billing, and enforcement. If one does not appear, the sensor is not seeing the request, which is the failure worth catching: a sensor reporting nothing looks exactly like a quiet site.


Behaviour you can rely on

  • It never throws. Every path is wrapped. A sensor that raises into your application is a broken site, and no amount of bot visibility is worth that.
  • It fails open. Unreachable, slow, or erroring: your request is served.
  • It is bounded. 200ms to connect, 400ms total, abandoned rather than retried. Under auto_prepend_file it reports at shutdown, after your response is already on its way.
  • It skips static assets. They are noise and they cost you volume.

Requirements

PHP 8.1+, ext-curl, ext-json.

8.1 is the real floor, not a preference: the package uses readonly promoted properties, which earlier versions cannot parse. Through v0.1.0 composer.json claimed 7.4, so Composer would install happily on 7.4 or 8.0 and the code then failed to parse. That matters more than a normal incompatibility here, because a parse error happens before any of this package's error handling can run: under auto_prepend_file it would take down every PHP request on the host. Declaring 8.1 makes Composer refuse the install instead.

Note for maintainers

The source of truth is php-sensor/ in the WebDecoy application monorepo. WebDecoy/php-sensor, which is what Composer and Packagist read, is an output of it.

Release from the monorepo:

cd php-sensor && ./release.sh 0.2.0

That checks Sensor::VERSION matches the tag, runs the tests, syncs, pushes, and tags. Do not edit WebDecoy/php-sensor directly: a change made there is lost on the next release, and a hand-copied mirror is the drift this project has already paid for once with the WordPress plugin.

This package is deliberately not bundled inside the WordPress plugin. WordPress.org's Plugin Check reports every curl_* call as an error, so a plugin shipping this file would fail review. Keeping the sensor standalone is what lets a WordPress plugin pass review while non-WordPress PHP can still report detections.

About

WebDecoy PHP Sensor — report bot and AI-crawler traffic to WebDecoy from any PHP application (Laravel, Symfony, Magento, or bespoke). Observes and reports; never blocks.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages