| @@ -48,8 +48,31 @@ | ||
| 48 | 48 | /** |
| 49 | 49 | * Initialize hooks |
| 50 | 50 | */ |
| 51 | 51 | private function init_hooks() { |
| 52 | + // XML-RPC exposure: one three-way setting. Lives here rather than under | |
| 53 | + // Login because this class already owns the xmlrpc_methods filter, and | |
| 54 | + // because the module gate for this tab is modules.wp_hardening: keeping | |
| 55 | + // the setting and the code it drives under the same gate avoids the trap | |
| 56 | + // of a switch that looks on while the class that reads it never runs. | |
| 57 | + $xmlrpc_mode = self::resolve_xmlrpc_mode( $this->settings ); | |
| 58 | + | |
| 59 | + if ( 'full' === $xmlrpc_mode ) { | |
| 60 | + // Answer the XML-RPC endpoint here, before core gets as far as | |
| 61 | + // building the server object. Until 2.9.9 this pointed the | |
| 62 | + // wp_xmlrpc_server_class filter at a class that does not exist, | |
| 63 | + // which is not a block but an uncaught Error: xmlrpc.php answered | |
| 64 | + // 500 with an empty body and wrote a PHP fatal to the log on every | |
| 65 | + // hit, and that endpoint is one of the most hammered by bots. | |
| 66 | + $this->block_xmlrpc_request(); | |
| 67 | + | |
| 68 | + add_filter( 'xmlrpc_enabled', '__return_false' ); | |
| 69 | + remove_action( 'wp_head', 'rsd_link' ); | |
| 70 | + remove_action( 'wp_head', 'wlwmanifest_link' ); | |
| 71 | + } elseif ( 'pingback' === $xmlrpc_mode ) { | |
| 72 | + add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) ); | |
| 73 | + } | |
| 74 | + | |
| 52 | 75 | // Disable pingbacks/trackbacks |
| 53 | 76 | if ( ! empty( $this->options['disable_pingbacks'] ) ) { |
| 54 | 77 | add_filter( 'xmlrpc_methods', array( $this, 'disable_pingback_methods' ) ); |
| 55 | 78 | add_filter( 'wp_headers', array( $this, 'remove_pingback_header' ) ); |
| @@ -92,8 +115,80 @@ | ||
| 92 | 115 | * |
| 93 | 116 | * @param array $methods XML-RPC methods. |
| 94 | 117 | * @return array |
| 95 | 118 | */ |
| 119 | + /** | |
| 120 | + * Resolve the XML-RPC mode. | |
| 121 | + * | |
| 122 | + * The setting used to be two independent checkboxes under Login that could | |
| 123 | + * be on at the same time and contradict each other ("disable everything" | |
| 124 | + * plus "disable only pingback"). It is now a single three-way choice stored | |
| 125 | + * in wp_hardening.xmlrpc_mode, next to the pingback settings it relates to. | |
| 126 | + * | |
| 127 | + * Sites upgrading have neither, only the old pair under login_security, so | |
| 128 | + * their choice is read from there and nothing changes for them until they | |
| 129 | + * save the tab. An install with none of the three gets 'full', which is what | |
| 130 | + * the old defaults did (disable_xmlrpc shipped on and took precedence). | |
| 131 | + * | |
| 132 | + * Public and static so this class, the settings screen and the Security | |
| 133 | + * Check all resolve the value the same way and cannot drift apart. | |
| 134 | + * | |
| 135 | + * @param Vigilante_Settings $settings Settings instance. | |
| 136 | + * @return string 'full', 'pingback' or 'none'. | |
| 137 | + */ | |
| 138 | + public static function resolve_xmlrpc_mode( $settings ) { | |
| 139 | + $hardening = $settings->get_section( 'wp_hardening' ); | |
| 140 | + $mode = isset( $hardening['xmlrpc_mode'] ) ? (string) $hardening['xmlrpc_mode'] : ''; | |
| 141 | + | |
| 142 | + if ( in_array( $mode, array( 'full', 'pingback', 'none' ), true ) ) { | |
| 143 | + return $mode; | |
| 144 | + } | |
| 145 | + | |
| 146 | + // Legacy pair under Login, only meaningful when one of them was stored. | |
| 147 | + $login = $settings->get_section( 'login_security' ); | |
| 148 | + if ( array_key_exists( 'disable_xmlrpc', $login ) | |
| 149 | + || array_key_exists( 'disable_xmlrpc_pingback', $login ) ) { | |
| 150 | + if ( ! empty( $login['disable_xmlrpc'] ) ) { | |
| 151 | + return 'full'; | |
| 152 | + } | |
| 153 | + if ( ! empty( $login['disable_xmlrpc_pingback'] ) ) { | |
| 154 | + return 'pingback'; | |
| 155 | + } | |
| 156 | + return 'none'; | |
| 157 | + } | |
| 158 | + | |
| 159 | + return 'full'; | |
| 160 | + } | |
| 161 | + | |
| 162 | + /** | |
| 163 | + * Answer an XML-RPC request with a plain 403 and stop | |
| 164 | + * | |
| 165 | + * Does nothing outside an XML-RPC request, so it is safe to call while the | |
| 166 | + * module is wiring its hooks. XMLRPC_REQUEST is defined at the top of | |
| 167 | + * xmlrpc.php, before wp-load.php, so it is already there by the time | |
| 168 | + * plugins load. | |
| 169 | + * | |
| 170 | + * The modules are built on init priority 1, which is inside init, so the | |
| 171 | + * translation functions are safe to use here. | |
| 172 | + * | |
| 173 | + * @since 2.9.9 | |
| 174 | + */ | |
| 175 | + private function block_xmlrpc_request() { | |
| 176 | + if ( ! defined( 'XMLRPC_REQUEST' ) || ! XMLRPC_REQUEST ) { | |
| 177 | + return; | |
| 178 | + } | |
| 179 | + | |
| 180 | + if ( ! headers_sent() ) { | |
| 181 | + status_header( 403 ); | |
| 182 | + nocache_headers(); | |
| 183 | + $charset = sanitize_text_field( (string) get_option( 'blog_charset', 'UTF-8' ) ); | |
| 184 | + header( 'Content-Type: text/plain; charset=' . ( '' !== $charset ? $charset : 'UTF-8' ) ); | |
| 185 | + } | |
| 186 | + | |
| 187 | + echo esc_html__( 'XML-RPC services are disabled on this site.', 'vigilante' ); | |
| 188 | + exit; | |
| 189 | + } | |
| 190 | + | |
| 96 | 191 | public function disable_pingback_methods( $methods ) { |
| 97 | 192 | unset( $methods['pingback.ping'] ); |
| 98 | 193 | unset( $methods['pingback.extensions.getPingbacks'] ); |
| 99 | 194 | return $methods; |