/, leaking the login slug). Hooking on `init` runs // before any canonical resolution. add_action( 'init', [ $this, 'block_author_enumeration' ], 0 ); add_filter( 'rest_endpoints', [ $this, 'maybe_remove_users_endpoint' ] ); add_filter( 'rest_user_query', [ $this, 'filter_rest_user_query' ] ); // oEmbed add_action( 'init', [ $this, 'maybe_disable_oembed' ], 99 ); // Self pingback add_action( 'pre_ping', [ $this, 'remove_self_pingbacks' ] ); // Headers add_filter( 'wp_headers', [ $this, 'security_headers' ], 100 ); // Math captcha add_action( 'login_form', [ $this, 'render_math_captcha' ] ); add_filter( 'authenticate', [ $this, 'verify_math_captcha' ], 30, 3 ); } // ───────────────────────── XML-RPC ────────────────────────────── public function filter_xmlrpc_enabled( $enabled ) { return GUARDFORGE_Options::get( 'disable_xmlrpc' ) ? false : $enabled; } public function block_xmlrpc_php() { if ( ! GUARDFORGE_Options::get( 'disable_xmlrpc' ) ) { return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only searched for a fixed needle, never stored or printed; sanitising would change what is matched. $uri = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); if ( strpos( $uri, 'xmlrpc.php' ) !== false ) { status_header( 403 ); nocache_headers(); wp_die( esc_html__( 'XML-RPC is disabled on this site.', 'guardforge' ), 'Forbidden', [ 'response' => 403 ] ); } } // ─────────────────────────── REST API ─────────────────────────── /** * Return WP_Error to block REST unless either: * - the user is logged in, or * - the requested route matches an allowlisted namespace. * * The allowlist prevents the original Lyuhon behaviour from breaking * block-editor previews, oEmbed, FormForge submissions, etc. */ public function filter_rest_authentication( $result ) { if ( ! GUARDFORGE_Options::get( 'disable_rest_api' ) ) { return $result; } if ( is_user_logged_in() ) { return $result; } if ( $this->current_rest_route_is_allowlisted() ) { return $result; } return new WP_Error( 'rest_not_logged_in', __( 'REST API is restricted to authenticated users.', 'guardforge' ), [ 'status' => 401 ] ); } private function current_rest_route_is_allowlisted() { $route = ''; if ( function_exists( 'rest_get_url_prefix' ) ) { $prefix = rest_get_url_prefix(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- only searched for the REST prefix and compared with the allowlist, never stored or printed; sanitising would change the route being judged. $path = wp_unslash( $_SERVER['REQUEST_URI'] ?? '' ); $needle = '/' . trim( $prefix, '/' ) . '/'; $pos = strpos( $path, $needle ); if ( $pos !== false ) { $route = ltrim( substr( $path, $pos + strlen( $needle ) ), '/' ); $qpos = strpos( $route, '?' ); if ( $qpos !== false ) { $route = substr( $route, 0, $qpos ); } } } if ( $route === '' ) { return false; } $allowlist = self::parse_lines( GUARDFORGE_Options::get( 'rest_allowlist', '' ) ); /** * Filter the REST-API allowlist (namespace prefixes). * * @param string $allowlist * @param string $route Current REST route relative to the prefix. */ $allowlist = apply_filters( 'guardforge/rest_allowlist', $allowlist, $route ); foreach ( $allowlist as $needle ) { if ( $needle === '' ) { continue; } if ( strpos( $route, $needle ) === 0 ) { return true; } } return false; } public function block_rest_comments( $prepared_comment, $request ) { if ( GUARDFORGE_Options::get( 'disable_rest_comments' ) ) { return new WP_Error( 'rest_comment_forbidden', __( 'Comments via REST are disabled.', 'guardforge' ), [ 'status' => 403 ] ); } return $prepared_comment; } // ───────────────────── Version / generator ────────────────────── public function filter_generator( $value ) { return GUARDFORGE_Options::get( 'hide_wp_version' ) ? '' : $value; } public function strip_version_query( $src ) { if ( ! GUARDFORGE_Options::get( 'hide_wp_version' ) ) { return $src; } return remove_query_arg( 'ver', $src ); } // ─────────────────────── File editor ──────────────────────────── public function maybe_disable_file_edit() { if ( GUARDFORGE_Options::get( 'disable_file_edit' ) && ! defined( 'DISALLOW_FILE_EDIT' ) ) { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- WordPress core's own constant, not ours: defining it is the documented way to switch off the theme and plugin file editor, which is exactly this setting. Guarded by defined above. define( 'DISALLOW_FILE_EDIT', true ); } } // ─────────────────────── User enumeration ─────────────────────── public function block_author_enumeration() { if ( ! GUARDFORGE_Options::get( 'disable_user_enumeration' ) ) { return; } if ( is_admin() ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- an anonymous front-end request; this reads the public ?author= query var only to refuse user enumeration, and changes nothing. if ( ! isset( $_REQUEST['author'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- same read-only check; is_numeric is the whole use of the value. if ( ! is_numeric( $_REQUEST['author'] ) ) { return; } wp_safe_redirect( home_url( '/' ), 301 ); exit; } public function maybe_remove_users_endpoint( $endpoints ) { if ( ! GUARDFORGE_Options::get( 'disable_user_enumeration' ) ) { return $endpoints; } if ( is_user_logged_in() ) { return $endpoints; } unset( $endpoints['/wp/v2/users'] ); unset( $endpoints['/wp/v2/users/(?P[\d]+)'] ); return $endpoints; } public function filter_rest_user_query( $args ) { if ( GUARDFORGE_Options::get( 'disable_user_enumeration' ) && ! is_user_logged_in() ) { // Force an empty set for any anonymous query that survived the // endpoint removal — e.g. via add_action wp_dropdown_users. $args['has_published_posts'] = false; $args['include'] = [ 0 ]; } return $args; } // ─────────────────────────── oEmbed ───────────────────────────── public function maybe_disable_oembed() { if ( ! GUARDFORGE_Options::get( 'disable_embed' ) ) { return; } remove_action( 'rest_api_init', 'wp_oembed_register_route' ); add_filter( 'embed_oembed_discover', '__return_false' ); remove_filter( 'oembed_dataparse', 'wp_filter_oembed_result', 10 ); remove_action( 'wp_head', 'wp_oembed_add_discovery_links' ); remove_action( 'wp_head', 'wp_oembed_add_host_js' ); } public function remove_self_pingbacks( &$links ) { if ( ! GUARDFORGE_Options::get( 'disable_pingback' ) ) { return; } $home = get_option( 'home' ); foreach ( $links as $k => $link ) { if ( strpos( $link, $home ) === 0 ) { unset( $links[ $k ] ); } } } // ─────────────────────────── Headers ──────────────────────────── public function security_headers( $headers ) { if ( GUARDFORGE_Options::get( 'security_headers' ) ) { $headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'; $headers['X-Content-Type-Options'] = 'nosniff'; $headers['X-Frame-Options'] = 'SAMEORIGIN'; $headers['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'; } if ( GUARDFORGE_Options::get( 'disable_xmlrpc' ) ) { unset( $headers['X-Pingback'] ); } if ( GUARDFORGE_Options::get( 'hide_wp_version' ) ) { unset( $headers['X-Powered-By'] ); } return $headers; } // ───────────────────────── Math captcha ───────────────────────── public function render_math_captcha() { if ( ! GUARDFORGE_Options::get( 'login_captcha_math' ) ) { return; } $a = wp_rand( 1, 9 ); $b = wp_rand( 1, 9 ); $sum = $a + $b; $key = wp_generate_password( 24, false ); set_transient( self::CAPTCHA_TRANSIENT . $key, $sum, 10 * MINUTE_IN_SECONDS ); ?>

Error: please answer the verification question.', 'guardforge' ) ); } // Read through to the pre-1.3.1 spelling so a login form rendered // just before the update can still be answered; both are dropped. $expected = get_transient( self::CAPTCHA_TRANSIENT . $key ); if ( $expected === false ) { $expected = get_transient( self::LEGACY_CAPTCHA_TRANSIENT . $key ); } delete_transient( self::CAPTCHA_TRANSIENT . $key ); delete_transient( self::LEGACY_CAPTCHA_TRANSIENT . $key ); if ( $expected === false ) { return new WP_Error( 'guardforge_captcha_expired', __( 'Error: verification expired, please try again.', 'guardforge' ) ); } if ( (int) $expected !== $answer ) { return new WP_Error( 'guardforge_captcha_wrong', __( 'Error: the verification answer was wrong.', 'guardforge' ) ); } return $user; } // ────────────────────────── Helpers ───────────────────────────── public static function parse_lines( $text ) { if ( ! is_string( $text ) || $text === '' ) { return []; } $out = []; foreach ( preg_split( '/[\r\n]+/', $text ) as $line ) { $line = trim( $line ); if ( $line === '' || $line[0] === '#' ) { continue; } $out[] = $line; } return $out; } }