initial import from https://downloads.wordpress.org/plugin/wp-fail2ban.4.2.5.zip
This commit is contained in:
193
feature/comments.php
Normal file
193
feature/comments.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Comment logging
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\notify_post_author' ) ) {
|
||||
/**
|
||||
* Log new comment
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param bool $maybe_notify
|
||||
* @param int $comment_ID
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @wp-f2b-extra Comment \d+
|
||||
*/
|
||||
function notify_post_author( $maybe_notify, $comment_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_LOG' );
|
||||
syslog( LOG_INFO, "Comment {$comment_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
return $maybe_notify;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'notify_post_author',
|
||||
__NAMESPACE__ . '\\notify_post_author',
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if ( defined( 'WP_FAIL2BAN_LOG_COMMENTS_EXTRA' ) ) {
|
||||
/** WPF2B_EVENT_COMMENT_NOT_FOUND */
|
||||
if ( WP_FAIL2BAN_LOG_COMMENTS_EXTRA & 0x20002 ) {
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\comment_id_not_found' ) ) {
|
||||
/**
|
||||
* Log attempted comment on non-existent post
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int $comment_post_ID
|
||||
*
|
||||
* @wp-f2b-extra Comment post not found \d+
|
||||
*/
|
||||
function comment_id_not_found( $comment_post_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_EXTRA_LOG' );
|
||||
syslog( LOG_NOTICE, "Comment post not found {$comment_post_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'comment_id_not_found', __NAMESPACE__ . '\\comment_id_not_found' );
|
||||
}
|
||||
|
||||
}
|
||||
/** LOG_ACTION_LOG_COMMENT_CLOSED */
|
||||
if ( WP_FAIL2BAN_LOG_COMMENTS_EXTRA & 0x20004 ) {
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\comment_closed' ) ) {
|
||||
/**
|
||||
* Log attempted comment on closed post
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int $comment_post_ID
|
||||
*
|
||||
* @wp-f2b-extra Comments closed on post \d+
|
||||
*/
|
||||
function comment_closed( $comment_post_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_EXTRA_LOG' );
|
||||
syslog( LOG_NOTICE, "Comments closed on post {$comment_post_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'comment_closed', __NAMESPACE__ . '\\comment_closed' );
|
||||
}
|
||||
|
||||
}
|
||||
/** LOG_ACTION_LOG_COMMENT_TRASH */
|
||||
if ( WP_FAIL2BAN_LOG_COMMENTS_EXTRA & 0x20008 ) {
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\comment_on_trash' ) ) {
|
||||
/**
|
||||
* Log attempted comment on trashed post
|
||||
*
|
||||
* @since 4.0.2 Fix message
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int $comment_post_ID
|
||||
*
|
||||
* @wp-f2b-extra Comment attempt on trash post \d+
|
||||
*/
|
||||
function comment_on_trash( $comment_post_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_EXTRA_LOG' );
|
||||
syslog( LOG_NOTICE, "Comment attempt on trash post {$comment_post_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'comment_on_trash', __NAMESPACE__ . '\\comment_on_trash' );
|
||||
}
|
||||
|
||||
}
|
||||
/** LOG_ACTION_LOG_COMMENT_DRAFT */
|
||||
if ( WP_FAIL2BAN_LOG_COMMENTS_EXTRA & 0x20010 ) {
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\comment_on_draft' ) ) {
|
||||
/**
|
||||
* Log attempted comment on draft post
|
||||
*
|
||||
* @since 4.0.2 Fix message
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int $comment_post_ID
|
||||
*
|
||||
* @wp-f2b-extra Comment attempt on draft post \d+
|
||||
*/
|
||||
function comment_on_draft( $comment_post_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_EXTRA_LOG' );
|
||||
syslog( LOG_NOTICE, "Comment attempt on draft post {$comment_post_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'comment_on_draft', __NAMESPACE__ . '\\comment_on_draft' );
|
||||
}
|
||||
|
||||
}
|
||||
/** LOG_ACTION_LOG_COMMENT_PASSWORD */
|
||||
if ( WP_FAIL2BAN_LOG_COMMENTS_EXTRA & 0x20020 ) {
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\comment_on_password_protected' ) ) {
|
||||
/**
|
||||
* Log attempted comment on password-protected post
|
||||
*
|
||||
* @since 4.0.2 Fix message
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param int $comment_post_ID
|
||||
*
|
||||
* @wp-f2b-extra Comment attempt on password-protected post \d+
|
||||
*/
|
||||
function comment_on_password_protected( $comment_post_ID )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_COMMENT_EXTRA_LOG' );
|
||||
syslog( LOG_NOTICE, "Comment attempt on password-protected post {$comment_post_ID}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'comment_on_password_protected', __NAMESPACE__ . '\\comment_on_password_protected' );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
144
feature/lib.php
Normal file
144
feature/lib.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Library functions
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* Wrapper for \openlog
|
||||
*
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
*
|
||||
* @param string $log
|
||||
*/
|
||||
function openlog( $log = 'WP_FAIL2BAN_AUTH_LOG' )
|
||||
{
|
||||
$tag = ( defined( 'WP_FAIL2BAN_SYSLOG_SHORT_TAG' ) && true === WP_FAIL2BAN_SYSLOG_SHORT_TAG ? 'wp' : 'wordpress' );
|
||||
$host = ( array_key_exists( 'WP_FAIL2BAN_HTTP_HOST', $_ENV ) ? $_ENV['WP_FAIL2BAN_HTTP_HOST'] : $_SERVER['HTTP_HOST'] );
|
||||
/**
|
||||
* Some varieties of syslogd have difficulty if $host is too long
|
||||
* @since 3.5.0
|
||||
*/
|
||||
if ( defined( 'WP_FAIL2BAN_TRUNCATE_HOST' ) && 1 < intval( WP_FAIL2BAN_TRUNCATE_HOST ) ) {
|
||||
$host = substr( $host, 0, intval( WP_FAIL2BAN_TRUNCATE_HOST ) );
|
||||
}
|
||||
|
||||
if ( false === \openlog( "{$tag}({$host})", WP_FAIL2BAN_OPENLOG_OPTIONS, constant( $log ) ) ) {
|
||||
error_log( 'WPf2b: Cannot open syslog', 0 );
|
||||
// @codeCoverageIgnore
|
||||
} elseif ( defined( 'WP_FAIL2BAN_TRACE' ) ) {
|
||||
error_log( 'WPf2b: Opened syslog', 0 );
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for \syslog
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param int $level
|
||||
* @param string $msg
|
||||
* @param string|null $remote_addr
|
||||
*/
|
||||
function syslog( $level, $msg, $remote_addr = null )
|
||||
{
|
||||
$msg .= ' from ';
|
||||
$msg .= ( is_null( $remote_addr ) ? remote_addr() : $remote_addr );
|
||||
|
||||
if ( false === \syslog( $level, $msg ) ) {
|
||||
error_log( "WPf2b: Cannot write to syslog: '{$msg}'", 0 );
|
||||
// @codeCoverageIgnore
|
||||
} elseif ( defined( 'WP_FAIL2BAN_TRACE' ) ) {
|
||||
error_log( "WPf2b: Wrote to syslog: '{$msg}'", 0 );
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
\closelog();
|
||||
if ( defined( 'PHPUNIT_COMPOSER_INSTALL' ) ) {
|
||||
echo "{$level}|{$msg}" ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Graceful immediate exit
|
||||
*
|
||||
* @since 4.0.5 Add JSON support
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
*
|
||||
* @param bool $is_json
|
||||
*/
|
||||
function bail( $is_json = false )
|
||||
{
|
||||
|
||||
if ( $is_json ) {
|
||||
return new \WP_Error( 403, 'Forbidden' );
|
||||
} else {
|
||||
wp_die( 'Forbidden', 'Forbidden', array(
|
||||
'response' => 403,
|
||||
) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute remote IP address
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @todo Test me!
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
function remote_addr()
|
||||
{
|
||||
static $remote_addr = null ;
|
||||
/**
|
||||
* @since 4.0.0
|
||||
*/
|
||||
|
||||
if ( is_null( $remote_addr ) ) {
|
||||
if ( defined( 'WP_FAIL2BAN_PROXIES' ) ) {
|
||||
|
||||
if ( array_key_exists( 'HTTP_X_FORWARDED_FOR', $_SERVER ) ) {
|
||||
$ip = ip2long( $_SERVER['REMOTE_ADDR'] );
|
||||
/**
|
||||
* PHP 7 lets you define an array
|
||||
* @since 3.5.4
|
||||
*/
|
||||
$proxies = ( is_array( WP_FAIL2BAN_PROXIES ) ? WP_FAIL2BAN_PROXIES : explode( ',', WP_FAIL2BAN_PROXIES ) );
|
||||
foreach ( $proxies as $proxy ) {
|
||||
|
||||
if ( '#' == $proxy[0] ) {
|
||||
continue;
|
||||
} elseif ( 2 == count( $cidr = explode( '/', $proxy ) ) ) {
|
||||
$net = ip2long( $cidr[0] );
|
||||
$mask = ~(pow( 2, 32 - $cidr[1] ) - 1);
|
||||
} else {
|
||||
$net = ip2long( $proxy );
|
||||
$mask = -1;
|
||||
}
|
||||
|
||||
if ( $net == ($ip & $mask) ) {
|
||||
return ( false === ($len = strpos( $_SERVER['HTTP_X_FORWARDED_FOR'], ',' )) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : substr( $_SERVER['HTTP_X_FORWARDED_FOR'], 0, $len ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* For plugins and themes that anonymise requests
|
||||
* @since 3.6.0
|
||||
*/
|
||||
$remote_addr = ( defined( 'WP_FAIL2BAN_REMOTE_ADDR' ) ? WP_FAIL2BAN_REMOTE_ADDR : $_SERVER['REMOTE_ADDR'] );
|
||||
}
|
||||
|
||||
return $remote_addr;
|
||||
}
|
||||
37
feature/password.php
Normal file
37
feature/password.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Password-related functionality
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\retrieve_password' ) ) {
|
||||
/**
|
||||
* Log password reset requests
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param string $user_login
|
||||
*
|
||||
* @wp-f2b-extra Password reset requested for .*
|
||||
*/
|
||||
function retrieve_password( $user_login )
|
||||
{
|
||||
openlog( 'WP_FAIL2BAN_PASSWORD_REQUEST_LOG' );
|
||||
syslog( LOG_NOTICE, "Password reset requested for {$user_login}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action( 'retrieve_password', __NAMESPACE__ . '\\retrieve_password' );
|
||||
}
|
||||
233
feature/plugins.php
Normal file
233
feature/plugins.php
Normal file
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Library functions
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.2.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* Hook: plugins_loaded
|
||||
*
|
||||
* @since 4.2.0
|
||||
*/
|
||||
function plugins_loaded()
|
||||
{
|
||||
do_action( 'wp_fail2ban_register' );
|
||||
}
|
||||
|
||||
add_action( 'plugins_loaded', __NAMESPACE__ . '\\plugins_loaded' );
|
||||
/**
|
||||
* Register plugin
|
||||
*
|
||||
* @since 4.2.0
|
||||
*
|
||||
* @param string $slug Plugin slug. This must be the actual plugin slug. Maximum length is 255 which should be more than enough.
|
||||
* @param string $name Plugin display name. This should be an unescaped string - HTML is allowed.
|
||||
*
|
||||
* @return int|false ID
|
||||
*/
|
||||
function register_plugin( $slug, $name )
|
||||
{
|
||||
global $wp_fail2ban, $wpdb ;
|
||||
if ( 255 < strlen( $slug ) ) {
|
||||
throw new \LengthException( 'slug too long' );
|
||||
}
|
||||
if ( 255 < strlen( $name ) ) {
|
||||
throw new \LengthException( 'name too long' );
|
||||
}
|
||||
if ( !is_array( @$wp_fail2ban['plugins'] ) ) {
|
||||
$wp_fail2ban['plugins'] = [];
|
||||
}
|
||||
if ( array_key_exists( $slug, $wp_fail2ban['plugins'] ) ) {
|
||||
return $wp_fail2ban['plugins'][$slug];
|
||||
}
|
||||
static $id = 0 ;
|
||||
return $wp_fail2ban['plugins'][$slug] = [
|
||||
'id' => ++$id,
|
||||
'name' => $name,
|
||||
'messages' => [],
|
||||
];
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_fail2ban_register_plugin',
|
||||
__NAMESPACE__ . '\\register_plugin',
|
||||
1,
|
||||
2
|
||||
);
|
||||
/**
|
||||
* Check if plugin is registered.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*
|
||||
* @param string $plugin_slug
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_registered_plugin( $plugin_slug )
|
||||
{
|
||||
global $wp_fail2ban ;
|
||||
return array_key_exists( $plugin_slug, $wp_fail2ban['plugins'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register plugin message.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*
|
||||
* @param string $plugin_slug
|
||||
* @param array $msg [
|
||||
* string slug: Message slug
|
||||
* string fail: hard|soft|extra
|
||||
* int facility: syslog facility
|
||||
* int priority: syslog priority
|
||||
* string event_class: Event Class
|
||||
* int event_id: Event ID
|
||||
* string message: Message with placeholders
|
||||
* HOST: Remote IP
|
||||
* USER: Current user name
|
||||
* array vars: Array of [name => regex] pairs
|
||||
*/
|
||||
function register_message( $plugin_slug, array $msg )
|
||||
{
|
||||
global $wp_fail2ban ;
|
||||
$event_classes = [
|
||||
'auth' => WPF2B_EVENT_CLASS_AUTH,
|
||||
'comment' => WPF2B_EVENT_CLASS_COMMENT,
|
||||
'password' => WPF2B_EVENT_CLASS_PASSWORD,
|
||||
'rest' => WPF2B_EVENT_CLASS_REST,
|
||||
'spam' => WPF2B_EVENT_CLASS_SPAM,
|
||||
'xmlrpc' => WPF2B_EVENT_CLASS_XMLRPC,
|
||||
'other' => 0,
|
||||
];
|
||||
$args = [];
|
||||
if ( !is_registered_plugin( $plugin_slug ) ) {
|
||||
throw new \InvalidArgumentException( 'plugin not registered' );
|
||||
}
|
||||
if ( !array_key_exists( 'slug', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'slug'" );
|
||||
}
|
||||
if ( !is_string( $msg['slug'] ) ) {
|
||||
throw new \InvalidArgumentException( "'slug' must be string" );
|
||||
}
|
||||
if ( !array_key_exists( 'fail', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'fail'" );
|
||||
}
|
||||
if ( !in_array( $msg['fail'], [ 'hard', 'soft', 'extra' ] ) ) {
|
||||
throw new \UnexpectedValueException( "'fail' must be one of 'hard', 'soft', 'extra'" );
|
||||
}
|
||||
$args['fail'] = $msg['fail'];
|
||||
if ( !array_key_exists( 'priority', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'priority'" );
|
||||
}
|
||||
if ( !in_array( $msg['priority'], [
|
||||
LOG_CRIT,
|
||||
LOG_ERR,
|
||||
LOG_WARNING,
|
||||
LOG_NOTICE,
|
||||
LOG_INFO,
|
||||
LOG_DEBUG
|
||||
] ) ) {
|
||||
throw new \UnexpectedValueException( "Invalid 'priority'" );
|
||||
}
|
||||
$args['priority'] = $msg['priority'];
|
||||
if ( !array_key_exists( 'event_class', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'event_class'" );
|
||||
}
|
||||
if ( !array_key_exists( $event_class = strtolower( $msg['event_class'] ), $event_classes ) ) {
|
||||
throw new \UnexpectedValueException( "Invalid 'event_class'" );
|
||||
}
|
||||
$args['class'] = $event_class;
|
||||
$event_class = $event_classes[$event_class];
|
||||
$log = sprintf( "WP_FAIL2BAN_%s_LOG", strtoupper( $event_class ) );
|
||||
if ( !array_key_exists( 'event_id', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'event_id'" );
|
||||
}
|
||||
if ( ($msg['event_id'] & 0xffff) !== $msg['event_id'] ) {
|
||||
throw new \UnexpectedValueException( "Invalid 'event_id'" );
|
||||
}
|
||||
$args['event_id'] = WPF2B_EVENT_TYPE_PLUGIN | $event_class | $msg['event_id'];
|
||||
if ( !array_key_exists( 'message', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'message'" );
|
||||
}
|
||||
if ( !is_string( $msg['message'] ) ) {
|
||||
throw new \UnexpectedValueException( "Invalid 'message'" );
|
||||
}
|
||||
$args['message'] = $msg['message'];
|
||||
if ( !array_key_exists( 'vars', $msg ) ) {
|
||||
throw new \InvalidArgumentException( "Missing 'vars'" );
|
||||
}
|
||||
if ( !is_array( $msg['vars'] ) ) {
|
||||
throw new \UnexpectedValueException( "Invalid 'vars'" );
|
||||
}
|
||||
$args['vars'] = $msg['vars'];
|
||||
$wp_fail2ban['plugins'][$plugin_slug]['messages'][$msg['slug']] = $args;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_fail2ban_register_message',
|
||||
__NAMESPACE__ . '\\register_message',
|
||||
1,
|
||||
2
|
||||
);
|
||||
/**
|
||||
* Check if message is registered.
|
||||
*
|
||||
* NB: Assumes plugin is registered.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*
|
||||
* @param string $plugin_slug
|
||||
* @param string $message_slug
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function is_registered_plugin_message( $plugin_slug, $message_slug )
|
||||
{
|
||||
global $wp_fail2ban ;
|
||||
return array_key_exists( $message_slug, $wp_fail2ban['plugins'][$plugin_slug]['messages'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log plugin message.
|
||||
*
|
||||
* @since 4.2.0
|
||||
*
|
||||
* @param string $plugin_slug Plugin slug for registered message
|
||||
* @param string $message_slug Message slug for registered message
|
||||
* @param array $vars Substitution vars
|
||||
*/
|
||||
function log_message( $plugin_slug, $message_slug = null, array $vars = array() )
|
||||
{
|
||||
global $wp_fail2ban ;
|
||||
if ( !is_registered_plugin( $plugin_slug ) ) {
|
||||
throw new \InvalidArgumentException( 'plugin not registered' );
|
||||
}
|
||||
if ( !is_registered_plugin_message( $plugin_slug, $message_slug ) ) {
|
||||
throw new \InvalidArgumentException( 'message not registered' );
|
||||
}
|
||||
$args = $wp_fail2ban['plugins'][$plugin_slug]['messages'][$message_slug];
|
||||
$msg = $args['message'];
|
||||
foreach ( $args['vars'] as $name => $regex ) {
|
||||
if ( array_key_exists( $name, $vars ) ) {
|
||||
$msg = str_replace( "___{$name}___", $vars[$name], $msg );
|
||||
}
|
||||
}
|
||||
openlog( sprintf( 'WP_FAIL2BAN_PLUGIN_%s_LOG', strtoupper( $args['class'] ) ) );
|
||||
syslog( $args['priority'], "({$plugin_slug}) {$msg}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_fail2ban_log_message',
|
||||
__NAMESPACE__ . '\\log_message',
|
||||
1,
|
||||
3
|
||||
);
|
||||
60
feature/spam.php
Normal file
60
feature/spam.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Spam comments
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\log_spam_comment' ) ) {
|
||||
/**
|
||||
* Catch comments marked as spam
|
||||
*
|
||||
* @since 3.5.0
|
||||
*
|
||||
* @param int $comment_id
|
||||
* @param string $comment_status
|
||||
*
|
||||
* @wp-f2b-hard Spam comment \d+
|
||||
*/
|
||||
function log_spam_comment( $comment_id, $comment_status )
|
||||
{
|
||||
if ( 'spam' === $comment_status ) {
|
||||
|
||||
if ( is_null( $comment = get_comment( $comment_id, ARRAY_A ) ) ) {
|
||||
/**
|
||||
* @todo: decide what to do about this
|
||||
*/
|
||||
} else {
|
||||
$remote_addr = ( empty($comment['comment_author_IP']) ? 'unknown' : $comment['comment_author_IP'] );
|
||||
openlog( 'WP_FAIL2BAN_SPAM_LOG' );
|
||||
syslog( LOG_NOTICE, "Spam comment {$comment_id}", $remote_addr );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
add_action(
|
||||
'comment_post',
|
||||
__NAMESPACE__ . '\\log_spam_comment',
|
||||
10,
|
||||
2
|
||||
);
|
||||
add_action(
|
||||
'wp_set_comment_status',
|
||||
__NAMESPACE__ . '\\log_spam_comment',
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
104
feature/user-enum.php
Normal file
104
feature/user-enum.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* User enumeration
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
if ( !function_exists( __NAMESPACE__ . '\\_log_bail_user_enum' ) ) {
|
||||
/**
|
||||
* Common enumeration handling
|
||||
*
|
||||
* @since 4.1.0 Add JSON support
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param bool $is_json
|
||||
*
|
||||
* @return \WP_Error
|
||||
*
|
||||
* @wp-f2b-hard Blocked user enumeration attempt
|
||||
*/
|
||||
function _log_bail_user_enum( $is_json = false )
|
||||
{
|
||||
openlog();
|
||||
syslog( LOG_NOTICE, 'Blocked user enumeration attempt' );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
return bail( $is_json );
|
||||
}
|
||||
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\parse_request' ) ) {
|
||||
/**
|
||||
* Catch traditional user enum
|
||||
*
|
||||
* @see \WP::parse_request()
|
||||
*
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
* @since 2.1.0
|
||||
*
|
||||
* @param \WP $query
|
||||
*
|
||||
* @return \WP
|
||||
*/
|
||||
function parse_request( $query )
|
||||
{
|
||||
if ( !current_user_can( 'list_users' ) && intval( @$query->query_vars['author'] ) ) {
|
||||
_log_bail_user_enum();
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'parse_request',
|
||||
__NAMESPACE__ . '\\parse_request',
|
||||
1,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\rest_user_query' ) ) {
|
||||
/**
|
||||
* Catch RESTful user list
|
||||
*
|
||||
* @see \WP_REST_Users_Controller::get_items()
|
||||
*
|
||||
* @since 4.0.0
|
||||
*
|
||||
* @param array $prepared_args
|
||||
* @param \WP_REST_Request $request
|
||||
*
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
function rest_user_query( $prepared_args, $request )
|
||||
{
|
||||
if ( !current_user_can( 'list_users' ) ) {
|
||||
return _log_bail_user_enum( true );
|
||||
}
|
||||
return $prepared_args;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'rest_user_query',
|
||||
__NAMESPACE__ . '\\rest_user_query',
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
61
feature/user.php
Normal file
61
feature/user.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Blocked user functionality
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\authenticate' ) ) {
|
||||
/**
|
||||
* Catched blocked users
|
||||
*
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
* @since 2.0.0
|
||||
*
|
||||
* @param mixed|null $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*
|
||||
* @return mixed|null
|
||||
*
|
||||
* @wp-f2b-hard Blocked authentication attempt for .*
|
||||
*/
|
||||
function authenticate( $user, $username, $password )
|
||||
{
|
||||
|
||||
if ( !empty($username) ) {
|
||||
/**
|
||||
* @since 3.5.0 Arrays allowed in PHP 7
|
||||
*/
|
||||
$matched = ( is_array( WP_FAIL2BAN_BLOCKED_USERS ) ? in_array( $username, WP_FAIL2BAN_BLOCKED_USERS ) : preg_match( '/' . WP_FAIL2BAN_BLOCKED_USERS . '/i', $username ) );
|
||||
|
||||
if ( $matched ) {
|
||||
openlog();
|
||||
syslog( LOG_NOTICE, "Blocked authentication attempt for {$username}" );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
bail();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
add_filter(
|
||||
'authenticate',
|
||||
__NAMESPACE__ . '\\authenticate',
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
108
feature/xmlrpc.php
Normal file
108
feature/xmlrpc.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* XML-RPC functionality
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\xmlrpc_login_error' ) ) {
|
||||
/**
|
||||
* Catch multiple XML-RPC authentication failures
|
||||
*
|
||||
* @see \wp_xmlrpc_server::login()
|
||||
*
|
||||
* @since 4.0.0 Return $error
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param \IXR_Error $error
|
||||
* @param \WP_Error $user
|
||||
*
|
||||
* @return \IXR_Error
|
||||
*
|
||||
* @wp-f2b-hard XML-RPC multicall authentication failure
|
||||
*/
|
||||
function xmlrpc_login_error( $error, $user )
|
||||
{
|
||||
static $attempts = 0 ;
|
||||
|
||||
if ( ++$attempts > 1 ) {
|
||||
openlog();
|
||||
syslog( LOG_NOTICE, 'XML-RPC multicall authentication failure' );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
bail();
|
||||
} else {
|
||||
return $error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action(
|
||||
'xmlrpc_login_error',
|
||||
__NAMESPACE__ . '\\xmlrpc_login_error',
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\xmlrpc_pingback_error' ) ) {
|
||||
/**
|
||||
* Catch failed pingbacks
|
||||
*
|
||||
* @see \wp_xmlrpc_server::pingback_error()
|
||||
*
|
||||
* @since 4.0.0 Return $ixr_error
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
* @since 3.0.0
|
||||
*
|
||||
* @param \IXR_Error $ixr_error
|
||||
*
|
||||
* @return \IXR_Error
|
||||
*
|
||||
* @wp-f2b-hard Pingback error .* generated
|
||||
*/
|
||||
function xmlrpc_pingback_error( $ixr_error )
|
||||
{
|
||||
|
||||
if ( 48 !== $ixr_error->code ) {
|
||||
openlog();
|
||||
syslog( LOG_NOTICE, 'Pingback error ' . $ixr_error->code . ' generated' );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
return $ixr_error;
|
||||
}
|
||||
|
||||
add_filter( 'xmlrpc_pingback_error', __NAMESPACE__ . '\\xmlrpc_pingback_error', 5 );
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0 Refactored
|
||||
* @since 2.2.0
|
||||
*/
|
||||
if ( defined( 'WP_FAIL2BAN_LOG_PINGBACKS' ) && true === WP_FAIL2BAN_LOG_PINGBACKS ) {
|
||||
require_once 'xmlrpc/pingback.php';
|
||||
}
|
||||
/**
|
||||
* @since 4.0.0 Refactored
|
||||
* @since 3.6.0
|
||||
*/
|
||||
if ( defined( 'WP_FAIL2BAN_XMLRPC_LOG' ) && '' < WP_FAIL2BAN_XMLRPC_LOG ) {
|
||||
require_once 'xmlrpc/log.php';
|
||||
}
|
||||
35
feature/xmlrpc/log.php
Normal file
35
feature/xmlrpc/log.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* XML-RPC Request logging
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log XML-RPC requests
|
||||
*
|
||||
* It seems attackers are doing weird things with XML-RPC. This makes it easy to
|
||||
* log them for analysis and future blocking.
|
||||
*
|
||||
* @since 4.0.0 Fix: Removed HTTP_RAW_POST_DATA
|
||||
* https://wordpress.org/support/?p=10971843
|
||||
* @since 3.6.0
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
if (false === ($fp = fopen(WP_FAIL2BAN_XMLRPC_LOG, 'a+'))) {
|
||||
// TODO: decided whether to log this
|
||||
} else {
|
||||
$raw_data = (version_compare(PHP_VERSION, '7.0.0') >= 0)
|
||||
? file_get_contents('php://input')
|
||||
: $HTTP_RAW_POST_DATA;
|
||||
|
||||
fprintf($fp, "# ---\n# Date: %s\n# IP: %s\n\n%s\n", date(DATE_ATOM), remote_addr(), $raw_data);
|
||||
fclose($fp);
|
||||
}
|
||||
40
feature/xmlrpc/pingback.php
Normal file
40
feature/xmlrpc/pingback.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* pingback logging
|
||||
*
|
||||
* @package wp-fail2ban
|
||||
* @since 4.0.0
|
||||
*/
|
||||
namespace org\lecklider\charles\wordpress\wp_fail2ban;
|
||||
|
||||
if ( !defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
/**
|
||||
* @since 4.0.5 Guard
|
||||
*/
|
||||
|
||||
if ( !function_exists( __NAMESPACE__ . '\\xmlrpc_call' ) ) {
|
||||
/**
|
||||
* Log pingbacks
|
||||
*
|
||||
* @since 3.5.0 Refactored for unit testing
|
||||
* @since 2.2.0
|
||||
*
|
||||
* @param string $call
|
||||
*/
|
||||
function xmlrpc_call( $call )
|
||||
{
|
||||
|
||||
if ( 'pingback.ping' == $call ) {
|
||||
openlog( 'WP_FAIL2BAN_PINGBACK_LOG' );
|
||||
syslog( LOG_INFO, 'Pingback requested' );
|
||||
closelog();
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
add_action( 'xmlrpc_call', __NAMESPACE__ . '\\xmlrpc_call' );
|
||||
}
|
||||
Reference in New Issue
Block a user