Below is a production-grade lightweight SQLi Attack Notifier. Save it in wp-content/mu-plugins/sqli-attack-notifier.php (MU plugins run automatically) or wp-content/plugins/ and activate.
It inspects $_GET, $_POST, $_COOKIE, and REQUEST_URI for common SQLi patterns, logs the events into a DB table ({prefix}sqli_events), writes to error_log, emails the admin, and optionally sends alerts to a Slack webhook. Tune patterns to reduce false positives.
<?php
/**
* Plugin Name: SQLi Attack Notifier (Lightweight)
* Description: Detects suspicious SQL injection payload patterns, logs the event, sends email to admin, and posts to Slack (optional).
* Version: 1.1
* Author: Your Name
* NOTE: Place in wp-content/mu-plugins/ for always-on protection.
*/
if ( ! defined( 'ABSPATH' ) ) exit;
class WP_SQLi_Attack_Notifier {
private $admin_email;
private $slack_webhook = ''; // set to your Slack incoming webhook URL to enable Slack notifications
private $notify_threshold = 1; // number of matched suspicious inputs before action triggers
// Basic heuristic patterns (tune them)
private $patterns = [
"/\bUNION\b/i",
"/\bSELECT\b.*\bFROM\b/i",
"/\bDROP\b\s+\bTABLE\b/i",
"/\bINSERT\b\s+\bINTO\b/i",
"/\bUPDATE\b\s+\bSET\b/i",
"/\bDELETE\b\s+\bFROM\b/i",
"/\bOR\b\s+['\"]?1['\"]?\s*=\s*['\"]?1['\"]?/i",
"/--\s*$/", "/#\s*$/",
"/;--|;|\bCHAR\(|\bCAST\(|\bCONCAT\(/i",
"/sleep\(\s*\d+\s*\)/i",
"/benchmark\(/i",
"/\bEXEC\b/i",
"/\bINFORMATION_SCHEMA\b/i",
"/0x[0-9a-f]{2,}/i",
];
public function __construct() {
$this->admin_email = get_option( 'admin_email' );
add_action( 'init', [ $this, 'inspect_request' ], 0 );
register_activation_hook( __FILE__, [ $this, 'maybe_create_table' ] );
}
public function inspect_request() {
// Optionally skip trusted contexts to reduce false positives
if ( is_user_logged_in() && current_user_can( 'manage_options' ) ) {
return;
}
$sources = [
'_GET' => isset($_GET) ? wp_unslash($_GET) : [],
'_POST' => isset($_POST) ? wp_unslash($_POST) : [],
'_COOKIE' => isset($_COOKIE) ? wp_unslash($_COOKIE) : [],
'URI' => isset($_SERVER['REQUEST_URI']) ? wp_unslash($_SERVER['REQUEST_URI']) : '',
];
$matches = [];
$count = 0;
foreach ( $sources as $key => $payload ) {
if ( is_array( $payload ) ) {
foreach ( $payload as $name => $value ) {
$value = (string) $value;
if ( $value === '' ) continue;
$hit = $this->match_patterns( $value );
if ( $hit ) {
$count++;
$matches[] = [ 'source' => $key, 'field' => $name, 'value' => $this->shorten($value), 'pattern' => $hit ];
}
}
} else {
$value = (string) $payload;
if ( $value !== '' ) {
$hit = $this->match_patterns( $value );
if ( $hit ) {
$count++;
$matches[] = [ 'source' => $key, 'field' => '', 'value' => $this->shorten($value), 'pattern' => $hit ];
}
}
}
}
if ( $count >= $this->notify_threshold ) {
$event = $this->build_event( $matches );
$this->maybe_create_table();
$this->log_event_to_db( $event );
error_log( '[SQLi-Notifier] ' . wp_json_encode( $event ) );
$this->send_email( $event );
if ( ! empty( $this->slack_webhook ) ) $this->send_slack( $event );
}
}
private function match_patterns( $value ) {
foreach ( $this->patterns as $pattern ) {
if ( preg_match( $pattern, $value ) ) return $pattern;
}
return false;
}
private function build_event( $matches ) {
$ip = $this->get_ip();
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
$uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
return [
'time' => current_time( 'mysql' ),
'ip' => $ip,
'ua' => $ua,
'uri' => $uri,
'matches' => $matches,
];
}
private function log_event_to_db( $event ) {
global $wpdb;
$table = $wpdb->prefix . 'sqli_events';
$this->maybe_create_table();
$wpdb->insert(
$table,
[
'event_time' => $event['time'],
'ip' => $event['ip'],
'user_agent' => mb_substr($event['ua'], 0, 255),
'request_uri'=> mb_substr($event['uri'], 0, 255),
'payload' => wp_json_encode($event['matches']),
],
[ '%s','%s','%s','%s','%s' ]
);
}
private function send_email( $event ) {
$subject = '[ALERT] Possible SQLi attempt on ' . get_bloginfo( 'name' );
$body = "Time: {$event['time']}\nIP: {$event['ip']}\nURI: {$event['uri']}\nUA: {$event['ua']}\n\nMatches:\n";
foreach ( $event['matches'] as $m ) {
$body .= "- {$m['source']} [{$m['field']}] => {$m['value']} (pattern: {$m['pattern']})\n";
}
$body .= "\nInvestigate or block the IP if needed.";
wp_mail( $this->admin_email, $subject, $body );
}
private function send_slack( $event ) {
if ( empty($this->slack_webhook) ) return;
$text = "*SQLi Alert* on *" . get_bloginfo('name') . "*\nTime: {$event['time']}\nIP: {$event['ip']}\nURI: {$event['uri']}\nMatches: " . count($event['matches']);
$payload = [ 'text' => $text ];
wp_remote_post( $this->slack_webhook, [
'headers' => [ 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( $payload ),
'timeout' => 3,
] );
}
public function maybe_create_table() {
global $wpdb;
$table_name = $wpdb->prefix . 'sqli_events';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
event_time datetime NOT NULL,
ip varchar(45) NOT NULL,
user_agent varchar(255) NOT NULL,
request_uri varchar(255) NOT NULL,
payload longtext NOT NULL,
PRIMARY KEY (id),
KEY ip (ip),
KEY event_time (event_time)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
}
private function get_ip() {
if ( ! empty($_SERVER['HTTP_X_FORWARDED_FOR']) ) {
$ips = explode(',', wp_unslash($_SERVER['HTTP_X_FORWARDED_FOR']));
return trim($ips[0]);
} elseif ( ! empty($_SERVER['REMOTE_ADDR']) ) {
return sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR']));
}
return '0.0.0.0';
}
private function shorten( $val, $len = 200 ) {
$val = trim($val);
return (mb_strlen($val) > $len) ? mb_substr($val,0,$len).'...' : $val;
}
}
new WP_SQLi_Attack_Notifier();
How to configure & deploy
- Place file in wp-content/mu-plugins/ (always-on) or as a normal plugin.
- Edit the file to set $slack_webhook if you want Slack alerts.
- Monitor the DB table wp_sqli_events and error_log for events.
- Tune $patterns and $notify_threshold to reduce false positives.
- Optionally add automatic IP blocking logic (careful; can block legit clients).
Caveat: This plugin is heuristic-based — it detects common payloads but can generate false positives. Use a WAF (Cloudflare / ModSecurity / Sucuri) for stronger protection.
How to triage and respond when your notifier fires
- Review event details: timestamp, IP, user agent, request URI, matched payloads.
- Check whether the IP is legitimate: lookups, GeoIP, or known scanner lists.
- Block abusive IPs temporarily via firewall or .htaccess. For high confidence, block via WAF.
- Scan the site with WPScan, Wordfence, and Patchstack for known vulnerable plugins/themes.
- Patch and update affected plugins/themes/core immediately. CVE advisories often list remediation steps (example: CVE-2022-21661 required users to upgrade WordPress core).
- Rotate secrets (DB password, API keys) if you suspect a breach.
- Restore from backups if data integrity is compromised and investigate root cause.