Last active
August 29, 2015 14:04
-
-
Save zacscott/0b052fc6f5fd42d33a02 to your computer and use it in GitHub Desktop.
Checks HTTP requests for common attacks/exploits (XSS, XSRF and SQL injection).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
Plugin Name: Secman | |
Plugin URI: https://gist.github.com/zscott92/0b052fc6f5fd42d33a02 | |
Description: Checks HTTP requests for common attacks/exploits (XSS, XSRF and SQL injection). Just activate and forget - no configuration! | |
Version: 1.0 | |
License: MIT License | |
Author: Zachary Scott | |
Author URI: http://www.zacscott.net | |
*/ | |
namespace zacscott; | |
/** Checks if the referer domain from the given header is correct. */ | |
function checkRefferer( $header ) | |
{ | |
if ( isset( $_SERVER[ $header ] ) ) { | |
$header = $_SERVER[ $header ]; | |
$domain = $_SERVER[ 'SERVER_NAME' ]; | |
$pos = strpos( $header, $domain ); | |
return ( $pos === 7 || $pos === 8 ); // i.e. after http:// or https:// | |
} else { | |
return true; // can't determine - no header | |
} | |
} | |
/** | |
* Detects cross-site request forgeries for form submission. | |
* Reports a `possibleHack()` and `die`s immediately as there are few | |
* false positives. However there may be false negatives (including | |
* intentional ones!). | |
* | |
* Both the `HTTP_ORIGIN` and `HTTP_REFERER` headers are checked if present. | |
*/ | |
function detectXSRF() | |
{ | |
if ( $_SERVER[ 'REQUEST_METHOD' ] == 'POST' ) { // can't check GET request as may be legit | |
if ( !checkRefferer( 'HTTP_ORIGIN' ) ) // as of 2014 only implemented by chrome :-( | |
$header = 'HTTP_ORIGIN'; | |
if ( !checkRefferer( 'HTTP_REFERER' ) ) // all browsers | |
$header = 'HTTP_REFERER'; | |
// check and report if not match | |
if ( isset( $header ) ) { | |
possibleHack( "XSRF HTTP refferer check failed on header $header." ); | |
die( | |
'A potential cross-site request forgery has been detected. '. | |
'For security purposes you cannot interact with this website.' | |
); | |
} | |
} | |
} | |
/** Attempts to detect XSS and SQL injection attacks in the given string. */ | |
function checkUserInput( $name, $input ) | |
{ | |
if ( $input != null ) { | |
// detect attacks | |
detectXss( $name, $input ); | |
detectSqlInjection( $name, $input ); | |
} | |
} | |
/** Attempts to detect a xss attack, from user input. */ | |
function detectXss( $name, $input ) | |
{ | |
assert( $name != null ); | |
assert( $input != null ); | |
// check common xss keywords & tags | |
$d = preg_match( | |
'{<\s*script|<\s*img|<\s*a|<\s*embed|<\s*image|src\s*=|<!--|javascript:}i', | |
$input | |
); | |
// report if detected | |
if ( $d ) | |
futileHack( "XSS attack detected in input <$name>: \n$input" ); | |
return $d; | |
} | |
/** Attempts to detect a SQL injection attack, from user input. */ | |
function detectSqlInjection( $name, $input ) | |
{ | |
assert( $name != null ); | |
assert( $input != null ); | |
// common indicators | |
$d = preg_match( | |
'{^\s*\'\s*;|--\s*$|#\s*$|/\*\s*$}', | |
$input | |
); | |
// explicit keywords | |
$d |= preg_match( | |
'{^\s*[\'"]\s*(union|select|insert|drop|delete)}i', | |
$input | |
); | |
// report if detected | |
if ( $d ) | |
futileHack( "SQL injection detected in input <$name>: \n$input" ); | |
return $d; | |
} | |
/** Returns the clients ip address. */ | |
function clientIp() | |
{ | |
return $_SERVER[ 'REMOTE_ADDR' ]; | |
} | |
/** Builds a stack trace starting from outside the Security object. */ | |
function stacktrace() | |
{ | |
$s = ''; | |
$first = true; | |
$backtrace = debug_backtrace(); | |
foreach ( $backtrace as $e ) { | |
// skip if empty entry | |
if ( !isset( $e[ 'file' ] ) || !isset( $e[ 'line' ] ) ) | |
continue; | |
// caller info | |
$file = $e[ 'file' ]; | |
$line = $e[ 'line' ]; | |
$func = $e[ 'function' ]; | |
$class = isset( $e[ 'class' ] ) ? $e[ 'class' ] : ''; | |
$type = isset( $e[ 'type' ] ) ? $e[ 'type' ] : ''; | |
if ( $func != 'net\\zeddev\\' ) // skip call to stackTrace() | |
$s .= "\t$class$type$func() ($file:$line) \n"; | |
} | |
return $s; | |
} | |
/** Builds the log message for a security alert to the webmaster. */ | |
function logmsg( $msg ) | |
{ | |
$logmsg = "$msg\n"; | |
$logmsg .= "\n"; | |
$ip = clientIp(); | |
$ts = date( 'Y-m-d H:i:s', time() ); | |
$logmsg .= "$title \n"; | |
$logmsg .= "Time: $ts\n"; | |
$logmsg .= "IP $ip \n"; | |
// add user info | |
if ( is_user_logged_in() ) { | |
$user = wp_get_current_user(); | |
$logmsg .= 'User - '. $user->first_name .' '. $user->last_name; | |
$logmsg .= ' ('. $user->ID .")\n"; | |
} | |
$logmsg .= "\n"; | |
$logmsg .= | |
"Stacktrace:\n". stacktrace() . | |
"\$_SERVER[] = \n". print_r( $_SERVER, true ) . | |
"\$_GET[] = \n". print_r( $_GET, true ) . | |
"\$_POST[] = \n". print_r( $_POST, true ). | |
"\n\n"; | |
return $logmsg; | |
} | |
/** Reports a security vulnerability and/or hack attempt. */ | |
function report( $msg ) | |
{ | |
$email = get_option( 'admin_email' ); | |
error_log( $msg, 0 ); // log to 'log_error' | |
error_log( logmsg( $msg ), 1, $email ); // send webmaster an email | |
} | |
/** | |
* Quietly reports a futile hack against the system. Indicates a possible | |
* hack attempt which should not cause any damage but may may indicate an | |
* unscrupulous individual looking for a vulnerability in the system. | |
* | |
* @param $msg The message to report. Must not be `null`. | |
*/ | |
function futileHack( $msg = '' ) | |
{ | |
assert( $msg != null ); | |
$ip = clientIp(); | |
report( | |
"FUTILE HACK DETECTED - $msg\n". | |
"NOTE: It is a distinct possibility that this is a false positive,". | |
" however it may indicate an unscrupulous individual looking for a". | |
" vulnerability." | |
); | |
} | |
/** | |
* Quietly reports a possible hack in the system. The cause might be a false | |
* positive however it still indicates a security vulneratibility which | |
* should have been detected before the call to this method. Therefore this | |
* method should be called to report the `last-line-of-defence` checks. | |
* | |
* @param $msg The message to report. Must not be `null`. | |
*/ | |
function possibleHack( $msg = '' ) | |
{ | |
assert( $msg != null ); | |
$ip = clientIp(); | |
report( | |
"POSSIBLE HACK DETECTED - $msg\n". | |
"NOTE: It is a distinct possibility that this is a false positive,". | |
" however it still should not occur and therefore warrants further". | |
" investigation." | |
); | |
} | |
// run the auto check sequence once all plugins are loaded | |
add_action( 'plugins_loaded', function() { | |
if ( !empty($_GET) || !empty( $_POST ) ) { // skip if not postback | |
// check user/request input (for get) | |
foreach ( $_GET as $k => $v ) | |
checkUserInput($k, $v); | |
// same again for post | |
foreach ( $_POST as $k => $v ) | |
checkUserInput($k, $v); | |
// check for cross-site request forgeries | |
detectXSRF(); | |
} | |
} ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment