-
Notifications
You must be signed in to change notification settings - Fork 101
/
security.php
232 lines (183 loc) · 8.19 KB
/
security.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
<?php
/*
Plugin Name: VIP Security
Description: Various security enhancements
Author: Automattic
Version: 1.0
License: GPL version 2 or later - http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
*/
require_once( __DIR__ . '/security/class-lockout.php' );
require_once( __DIR__ . '/security/machine-user.php' );
require_once( __DIR__ . '/security/class-private-sites.php' );
define( 'CACHE_GROUP_LOGIN_LIMIT', 'login_limit' );
define( 'CACHE_GROUP_LOST_PASSWORD_LIMIT', 'lost_password_limit' );
define( 'ERROR_CODE_LOGIN_LIMIT_EXCEEDED', 'login_limit_exceeded' );
define( 'ERROR_CODE_LOST_PASSWORD_LIMIT_EXCEEDED', 'lost_password_limit_exceeded' );
// If the site has any privacy restrictions (enabled by constant, ip restriction, http basic auth), initialize the Private_Sites module
if ( \Automattic\VIP\Security\Private_Sites::has_privacy_restrictions() ) {
\Automattic\VIP\Security\Private_Sites::instance();
}
/**
* Enforces strict username sanitization.
*
* @param string $username
* @return string
*/
function vip_strict_sanitize_username( $username ) {
if ( is_email( $username ) ) {
// We don't want to do this strict filter on email addresses. Sanitize the email and return.
$username = sanitize_email( $username );
return $username;
}
$username = sanitize_user( $username, true );
return $username;
}
function wpcom_vip_is_restricted_username( $username ) {
return 'admin' === $username
|| WPCOM_VIP_MACHINE_USER_LOGIN === $username
|| WPCOM_VIP_MACHINE_USER_EMAIL === $username;
}
/**
* Tracks and caches IP and IP|Username events.
*
* @param string $username The username to track.
* @param string $cache_group The cache group to track the $username to.
*/
function wpcom_vip_track_auth_attempt( $username, $cache_group, $cache_expiry ) {
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
$ip_username_cache_key = $ip . '|' . $username; // IP + username
$ip_cache_key = $ip; // IP only
// Longer TTL when logging in as admin, which we don't allow on WP.com
$is_restricted_username = wpcom_vip_is_restricted_username( $username );
if ( $is_restricted_username ) {
$cache_expiry = HOUR_IN_SECONDS + $cache_expiry;
}
wp_cache_add( $ip_username_cache_key, 0, $cache_group, $cache_expiry );
wp_cache_add( $ip_cache_key, 0, $cache_group, HOUR_IN_SECONDS );
wp_cache_incr( $ip_username_cache_key, 1, $cache_group );
wp_cache_incr( $ip_cache_key, 1, $cache_group );
}
function wpcom_vip_login_limiter( $username ) {
// Do some extra sanitization on the username.
$username = vip_strict_sanitize_username( $username );
wpcom_vip_track_auth_attempt( $username, CACHE_GROUP_LOGIN_LIMIT, MINUTE_IN_SECONDS * 5 );
}
add_action( 'wp_login_failed', 'wpcom_vip_login_limiter' );
function wpcom_vip_login_limiter_on_success( $username, $user ) {
// Do some extra sanitization on the username.
$username = vip_strict_sanitize_username( $username );
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
$ip_username_cache_key = $ip . '|' . $username; // IP + username
$ip_cache_key = $ip; // IP only
wp_cache_decr( $ip_username_cache_key, 1, CACHE_GROUP_LOGIN_LIMIT );
wp_cache_decr( $ip_cache_key, 1, CACHE_GROUP_LOGIN_LIMIT );
}
add_action( 'wp_login', 'wpcom_vip_login_limiter_on_success', 10, 2 );
function wpcom_vip_limit_logins_for_restricted_usernames( $user, $username, $password ) {
$is_restricted_username = wpcom_vip_is_restricted_username( $username );
if ( $is_restricted_username ) {
return new WP_Error( 'restricted-login', 'Logins are restricted for that user. Please try a different user account.' );
}
return $user;
}
add_filter( 'authenticate', 'wpcom_vip_limit_logins_for_restricted_usernames', 30, 3 ); // core authenticates on 20
function wpcom_vip_login_limiter_authenticate( $user, $username, $password ) {
if ( empty( $username ) && empty( $password ) )
return $user;
// Do some extra sanitization on the username.
$username = vip_strict_sanitize_username( $username );
$is_login_limited = wpcom_vip_username_is_limited( $username, CACHE_GROUP_LOGIN_LIMIT );
if ( is_wp_error( $is_login_limited ) ) {
return $is_login_limited;
}
return $user;
}
add_filter( 'authenticate', 'wpcom_vip_login_limiter_authenticate', 30, 3 ); // core authenticates on 20
function wpcom_vip_login_limit_dont_show_login_form() {
if ( 'post' != strtolower( $_SERVER['REQUEST_METHOD'] ) || !isset( $_POST['log'] ) ) {
return;
}
// Do some sanitization on the username.
$username = vip_strict_sanitize_username( $_POST['log'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( $error = wpcom_vip_username_is_limited( $username, CACHE_GROUP_LOGIN_LIMIT ) ) {
login_header( __( 'Error' ), '', $error );
login_footer();
exit;
}
}
add_action( 'login_form_login', 'wpcom_vip_login_limit_dont_show_login_form' );
function wpcom_vip_login_limit_xmlrpc_error( $error, $user ) {
static $login_limit_error;
if ( is_wp_error( $user ) && ERROR_CODE_LOGIN_LIMIT_EXCEEDED === $user->get_error_code() ) {
// We need to set a persistent error here, as once there is an auth error in a system.multicall, core will no longer trigger any of the rate limit filters for further login attempts in the set.
$login_limit_error = $user;
}
if ( is_wp_error( $login_limit_error ) ) {
return new IXR_Error( 429, $login_limit_error->get_error_message() );
}
return $error;
}
add_filter( 'xmlrpc_login_error', 'wpcom_vip_login_limit_xmlrpc_error', 10, 2 );
function wpcom_set_status_header_on_xmlrpc_failed_login_requests( $error ) {
header( "X-XMLRPC-Error-Code: {$error->code}" );
return $error;
}
add_action( 'xmlrpc_login_error', 'wpcom_set_status_header_on_xmlrpc_failed_login_requests' );
function wpcom_vip_lost_password_limit( $errors ) {
// Don't bother checking if we're already error-ing out
if ( $errors->get_error_code() ) {
return $errors;
}
// Do some sanitization on the username.
// ignoring WordPress.Security.NonceVerification.Missing and WordPress.Security.ValidatedSanitizedInput.InputNotValidated below
$username = vip_strict_sanitize_username( $_POST['user_login'] ); // phpcs:ignore
$is_login_limited = wpcom_vip_username_is_limited( $username, CACHE_GROUP_LOST_PASSWORD_LIMIT );
if ( is_wp_error( $is_login_limited ) ) {
$errors->add( $is_login_limited->get_error_code(), $is_login_limited->get_error_message() );
return $errors;
}
wpcom_vip_track_auth_attempt( $username, CACHE_GROUP_LOST_PASSWORD_LIMIT, MINUTE_IN_SECONDS * 30 );
return $errors;
}
add_action( 'lostpassword_post', 'wpcom_vip_lost_password_limit' );
function wpcom_vip_username_is_limited( $username, $cache_group ) {
// Strip invalid characters from the address
$ip = preg_replace( '/[^0-9a-fA-F:., ]/', '', $_SERVER['REMOTE_ADDR'] );
$ip_username_cache_key = $ip . '|' . $username;
$ip_cache_key = $ip;
/**
* Login Limiting IP Username Threshold
*
* @param string $ip IP address of the login request
* @param string $username Username of the login request
*/
$ip_username_threshold = apply_filters( 'wpcom_vip_ip_username_login_threshold', 5, $ip, $username );
/**
* Login Limiting IP Threshold
*
* @param string $ip IP address of the login request
*/
$ip_threshold = apply_filters( 'wpcom_vip_ip_login_threshold', 50, $ip );
$ip_username_count = wp_cache_get( $ip_username_cache_key, $cache_group );
$ip_count = wp_cache_get( $ip_cache_key, $cache_group );
$is_restricted_username = wpcom_vip_is_restricted_username( $username );
if ( 'lost_password_limit' === $cache_group ) {
$ip_username_threshold = 3;
$ip_threshold = 3;
} elseif ( $is_restricted_username ) {
$ip_username_threshold = 2;
}
if ( $ip_username_count >= $ip_username_threshold || $ip_count >= $ip_threshold ) {
switch ( $cache_group ) {
case CACHE_GROUP_LOST_PASSWORD_LIMIT:
do_action( 'password_reset_limit_exceeded', $username );
return new WP_Error( ERROR_CODE_LOST_PASSWORD_LIMIT_EXCEEDED, __( 'You have exceeded the password reset limit. Please wait a few minutes and try again.' ) );
break;
case CACHE_GROUP_LOGIN_LIMIT:
do_action( 'login_limit_exceeded', $username );
return new WP_Error( ERROR_CODE_LOGIN_LIMIT_EXCEEDED, __( 'You have exceeded the login limit. Please wait a few minutes and try again.' ) );
break;
}
}
return false;
}