PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.8
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.8
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
← All changes | includes/class-settings.php +2 -305 3.0.02.11.8 View file →
@@ -93,14 +93,8 @@
93 93 // Proxy / CDN: forwarded header to trust for the visitor IP.
94 94 // Empty = trust only REMOTE_ADDR (the real connection, unspoofable).
95 95 'trusted_proxy_header' => '',
96 96
97 - // Proxy / CDN: IPs or CIDR ranges the forwarded header is accepted
98 - // from. Empty = accept it only from your own network, and, for
99 - // CF-Connecting-IP, Cloudflare's own ranges. Since 2.11.9, so a
100 - // header cannot be forged by a visitor reaching the origin directly.
101 - 'trusted_proxies' => array(),
102 -
103 97 // User-Agent management
104 98 'ua_whitelist' => array(),
105 99 'ua_blacklist' => array(),
106 100
@@ -790,15 +784,8 @@
790 784 * addresses that receive the alerts is another, and nobody presses a button
791 785 * called "restore defaults" expecting that. Both the Standard preset and the
792 786 * two reset buttons leave these alone.
793 787 *
794 - * Read from outside the plugin: the third-party network plugin Vigilante
795 - * Network Sync calls this from its 2.0.3 to decide what NOT to copy between
796 - * the sites of a network, so a key it does not know about is preserved per
797 - * site instead of overwritten. Adding keys here is safe and helps it;
798 - * renaming or removing the method, or changing the shape of what it returns,
799 - * silently changes what that plugin replicates across a whole network.
800 - *
801 788 * @since 2.9.8
802 789 *
803 790 * @return array<string,string[]>
804 791 */
@@ -803,9 +790,9 @@
803 790 * @return array<string,string[]>
804 791 */
805 792 public static function get_user_data_keys() {
806 793 return array(
807 - 'firewall' => array( 'ip_whitelist', 'ip_blacklist', 'ua_whitelist', 'ua_blacklist', 'trusted_proxy_header', 'trusted_proxies' ),
794 + 'firewall' => array( 'ip_whitelist', 'ip_blacklist', 'ua_whitelist', 'ua_blacklist', 'trusted_proxy_header' ),
808 795 'login_security' => array( 'ip_whitelist', 'custom_login_url', 'two_factor' ),
809 796 'user_security' => array( 'insecure_usernames' ),
810 797 'file_integrity' => array( 'excluded_paths', 'excluded_extensions' ),
811 798 'email' => array( 'additional_recipients' ),
@@ -862,12 +849,8 @@
862 849 * rights must not be able to silence it by turning either one off. Closing
863 850 * the ignore list and the clear-results button in 2.11.8 left these two as
864 851 * the remaining routes; found by the audit of the admin surface.
865 852 *
866 - * Since 3.0.0 the self-check has no setting at all, so there is nothing to lock: Vigilant's own
867 - * files are shared by every site, and on the main site the self-check is
868 - * the one that reports a change to them for the whole network.
869 - *
870 853 * On a subsite all of them only act on that site, so they stay editable
871 854 * there (get_locked_file_settings() adds this set only when owns_shared_files()).
872 855 *
873 856 * Until 2.11.6 an administrator of the main site without network rights
@@ -880,278 +863,12 @@
880 863 * @since 2.11.8 The file_integrity module and scan_critical_config.
881 864 *
882 865 * @return array<string,string[]>
883 866 */
884 - /**
885 - * The two factor policy that governs this installation
886 - *
887 - * On a network the answer must not depend on which site the login happens to
888 - * arrive at, because the cookie WordPress issues does not: COOKIEHASH comes
889 - * from the network siteurl (wp-includes/default-constants.php) and
890 - * COOKIE_DOMAIN covers every host of the network
891 - * (wp-includes/ms-default-constants.php). Until 2.11.10 the settings, the
892 - * enforced roles and the TOTP table were all read per site, so somebody
893 - * holding the password of an administrator protected by 2FA on the main site
894 - * posted the login to a subsite where that account has no role, was never
895 - * asked for a code, and came out with a session valid across the network.
896 - * Measured on the Multisite install on 12 sep 2026 (user_requires_2fa true on
897 - * the main site, false on demo2 for the same account) and found by the
898 - * file-by-file review of 2.11.10.
899 - *
900 - * So the policy of the main site governs the whole network. On a single site
901 - * this is the site's own configuration and nothing changes.
902 - *
903 - * @since 2.11.10
904 - *
905 - * @return array The two_factor section that applies.
906 - */
907 - public static function two_factor_policy() {
908 - $options = is_multisite()
909 - ? get_blog_option( get_main_site_id(), self::OPTION_NAME, array() )
910 - : get_option( self::OPTION_NAME, array() );
911 -
912 - if ( ! is_array( $options ) ) {
913 - return array();
914 - }
915 -
916 - $login = isset( $options['login_security'] ) && is_array( $options['login_security'] )
917 - ? $options['login_security']
918 - : array();
919 -
920 - return ( isset( $login['two_factor'] ) && is_array( $login['two_factor'] ) ) ? $login['two_factor'] : array();
921 - }
922 -
923 - /**
924 - * Whether two factor is required for this account, anywhere in the network
925 - *
926 - * Union, and deliberately so. Reading the policy only from the main site,
927 - * which is what this release did at first, would have switched two factor off
928 - * for every network that has it configured per subsite, which until now was
929 - * the only way it could be configured at all: a silent downgrade of the very
930 - * protection being fixed. Found by the cross review of 2.11.10. So the
931 - * question is asked of every site the account belongs to, plus the main site,
932 - * each with its own enforced roles and exclusions, and one yes is enough.
933 - *
934 - * That also closes the bypass: the settings, the roles and the enrolment used
935 - * to be read from whichever site the login arrived at, while the cookie
936 - * WordPress issues is valid across the whole network (COOKIEHASH comes from
937 - * the network siteurl and COOKIE_DOMAIN covers every host of it), so an
938 - * account protected on one site could log in through another and come out
939 - * with a session valid everywhere.
940 - *
941 - * @since 2.11.10
942 - *
943 - * @param WP_User $user User being authenticated.
944 - * @return bool
945 - */
946 - public static function two_factor_required_for( $user ) {
947 - return array() !== self::two_factor_demands_for( $user );
948 - }
949 -
950 - /**
951 - * Which second factor methods this account is asked for, across the network
952 - *
953 - * Empty when nothing asks. On a network there can be more than one, because
954 - * each site keeps its own settings and the requirement is the union of them.
955 - *
956 - * @since 2.11.10
957 - *
958 - * @param WP_User $user User being authenticated.
959 - * @return string[] Methods asked for, without repeats.
960 - */
961 - public static function two_factor_methods_for( $user ) {
962 - $methods = array();
963 -
964 - foreach ( self::two_factor_demands_for( $user ) as $settings ) {
965 - $method = isset( $settings['method'] ) ? (string) $settings['method'] : 'email';
966 -
967 - // A method this version does not know is read as the default rather
968 - // than left to fall through. Registering nothing at all is how the
969 - // second factor of a whole network went quiet in silence: a saved
970 - // value of '' (which validate_section() lets through on an import,
971 - // since only the tab has an allowlist) matched neither class, so no
972 - // filter was registered anywhere while every screen still said two
973 - // factor was on. Found by the third cross review of 2.11.10.
974 - $methods[] = in_array( $method, array( 'email', 'totp' ), true ) ? $method : 'email';
975 - }
976 -
977 - return array_values( array_unique( $methods ) );
978 - }
979 -
980 - /**
981 - * Which of the two second factor classes handles this login
982 - *
983 - * The method cannot be read from one site's settings, and reading it from the
984 - * main site was the hole the third cross review of 2.11.10 found: with the
985 - * main site on totp and a subsite asking for a code by email, the class that
986 - * registered was TOTP, the account had no enrolment, and the "not set up yet"
987 - * branch let the login through. In 2.11.9 that same login was asked for its
988 - * emailed code. So the question is asked per account, not per site.
989 - *
990 - * The order is what keeps it closed at both ends:
991 - *
992 - * 1. An enrolment already made wins while some site asking for a second
993 - * factor asks for an authenticator app. It is the strongest factor the
994 - * account has and it is ready to use, wherever in the network it was set
995 - * up.
996 - * 2. Otherwise, if any site asking for a second factor asks for email, email
997 - * handles it. Email needs no enrolment, so it can never fall into the
998 - * branch that lets a login through for lack of one.
999 - * 3. Only when every site asking wants an authenticator app does TOTP handle
1000 - * it, which is the case the grace period was written for.
1001 - *
1002 - * The condition on the first step came in 2.11.11. Without it an enrolment
1003 - * left from a time when the site asked for an app outranked the method the
1004 - * site asks for now: a single site set to email asked those accounts for an
1005 - * authenticator code, which 2.11.9 never did and which locks out whoever
1006 - * removed the app after the switch. Dropping that enrolment opens nothing,
1007 - * because the account then goes to email, which needs no enrolment.
1008 - *
1009 - * @since 2.11.10
1010 - * @since 2.11.11 An enrolment only wins while an authenticator app is asked for.
1011 - *
1012 - * @param WP_User $user User being authenticated.
1013 - * @param bool $enrolled Whether the account has a TOTP enrolment anywhere.
1014 - * @return string 'email', 'totp', or '' when nothing asks.
1015 - */
1016 - public static function two_factor_handler_for( $user, $enrolled ) {
1017 - $methods = self::two_factor_methods_for( $user );
1018 -
1019 - if ( ! $methods ) {
1020 - return '';
1021 - }
1022 -
1023 - if ( $enrolled && in_array( 'totp', $methods, true ) ) {
1024 - return 'totp';
1025 - }
1026 -
1027 - return in_array( 'email', $methods, true ) ? 'email' : 'totp';
1028 - }
1029 -
1030 - /**
1031 - * The two factor settings of every site that asks this account for one
1032 - *
1033 - * @since 2.11.10
1034 - *
1035 - * @param WP_User $user User being authenticated.
1036 - * @return array[] The two_factor section of each site that asks.
1037 - */
1038 - private static function two_factor_demands_for( $user ) {
1039 - if ( empty( $user->ID ) ) {
1040 - return array();
1041 - }
1042 -
1043 - if ( ! is_multisite() ) {
1044 - $roles = ( isset( $user->roles ) && is_array( $user->roles ) ) ? $user->roles : array();
1045 - $policy = self::two_factor_policy();
1046 -
1047 - return self::two_factor_site_requires( $policy, $user, $roles ) ? array( $policy ) : array();
1048 - }
1049 -
1050 - $demands = array();
1051 - $blog_ids = array( (int) get_main_site_id() );
1052 -
1053 - /*
1054 - * A super administrator is a member of almost no site (measured on the
1055 - * Multisite install: of three sites, the network owner belongs to one),
1056 - * yet can log in through any of them and the cookie is valid everywhere.
1057 - * Asking only the sites they belong to left the account with the most
1058 - * power in the network outside the policy, which is the bypass upside
1059 - * down. So for them every site of the network is consulted. There are
1060 - * few super administrators. It does not only run at login, though, which
1061 - * this note claimed until 2.11.11: the dashboard hooks of the TOTP class
1062 - * ask it on every admin screen of every site of a network, at least once
1063 - * per hook.
1064 - */
1065 - if ( is_super_admin( $user->ID ) ) {
1066 - $blog_ids = array_merge( $blog_ids, get_sites( array( 'fields' => 'ids', 'number' => 200 ) ) );
1067 - }
1068 -
1069 - foreach ( get_blogs_of_user( $user->ID ) as $blog ) {
1070 - if ( ! empty( $blog->userblog_id ) ) {
1071 - $blog_ids[] = (int) $blog->userblog_id;
1072 - }
1073 - }
1074 -
1075 - foreach ( array_unique( $blog_ids ) as $blog_id ) {
1076 - $options = get_blog_option( $blog_id, self::OPTION_NAME, array() );
1077 -
1078 - if ( ! is_array( $options ) || empty( $options['login_security']['two_factor'] ) ) {
1079 - continue;
1080 - }
1081 -
1082 - /*
1083 - * A site whose Login Security module is off asks for nothing, and
1084 - * reading only the sub-setting made it ask anyway: a subsite that had
1085 - * switched the whole module off, leaving an orphan two_factor.enabled
1086 - * behind, imposed a second factor on every account of the network,
1087 - * with no screen anywhere explaining why. It is the two-level toggle
1088 - * trap of this plugin read upside down. Found by the third cross
1089 - * review of 2.11.10.
1090 - */
1091 - if ( empty( $options['modules']['login_security'] ) ) {
1092 - continue;
1093 - }
1094 -
1095 - $elsewhere = new WP_User( $user->ID );
1096 - $elsewhere->for_site( $blog_id );
1097 -
1098 - // A super administrator can hold no role row anywhere, and is judged
1099 - // as an administrator so the strictest policy of the network reaches
1100 - // the account with the most power in it.
1101 - $roles = ( isset( $elsewhere->roles ) && is_array( $elsewhere->roles ) && $elsewhere->roles )
1102 - ? $elsewhere->roles
1103 - : ( is_super_admin( $user->ID ) ? array( 'administrator' ) : array() );
1104 -
1105 - if ( self::two_factor_site_requires( $options['login_security']['two_factor'], $user, $roles ) ) {
1106 - $demands[] = $options['login_security']['two_factor'];
1107 - }
1108 - }
1109 -
1110 - return $demands;
1111 - }
1112 -
1113 - /**
1114 - * Whether one site's two factor settings cover this account
1115 - *
1116 - * @since 2.11.10
1117 - *
1118 - * @param array $settings The two_factor section of one site.
1119 - * @param WP_User $user User being authenticated.
1120 - * @param string[] $roles Roles the account holds on that site.
1121 - * @return bool
1122 - */
1123 - private static function two_factor_site_requires( $settings, $user, $roles ) {
1124 - if ( ! is_array( $settings ) || empty( $settings['enabled'] ) ) {
1125 - return false;
1126 - }
1127 -
1128 - $excluded = isset( $settings['excluded_users'] ) ? array_map( 'absint', (array) $settings['excluded_users'] ) : array();
1129 -
1130 - if ( in_array( (int) $user->ID, $excluded, true ) ) {
1131 - return false;
1132 - }
1133 -
1134 - $enforced = isset( $settings['enforced_roles'] ) ? (array) $settings['enforced_roles'] : array( 'administrator', 'editor' );
1135 -
1136 - return (bool) array_intersect( (array) $roles, $enforced );
1137 - }
1138 -
1139 - /**
1140 - * Settings that only a network administrator may change on the main site
1141 - *
1142 - * @return array<string,string[]>
1143 - */
1144 867 public static function get_main_site_file_settings() {
1145 868 return array(
1146 869 'modules' => array( 'firewall', 'security_headers', 'wp_hardening', 'file_integrity' ),
1147 - // trusted_proxies goes with trusted_proxy_header, and leaving it out
1148 - // was a hole: the header decides which address the firewall of the
1149 - // whole installation acts on, and this list decides which peers may
1150 - // set that header. An administrator of the main site without network
1151 - // rights who could edit only this half turned every visitor into a
1152 - // trusted proxy. Found by the file-by-file review of 2.11.10.
1153 - 'firewall' => array( 'block_bad_bots', 'block_bad_query_strings', 'trusted_proxy_header', 'trusted_proxies', 'ip_whitelist', 'ua_whitelist' ),
870 + 'firewall' => array( 'block_bad_bots', 'block_bad_query_strings', 'trusted_proxy_header', 'ip_whitelist', 'ua_whitelist' ),
1154 871 'file_integrity' => array( 'scan_critical_config' ),
1155 872 );
1156 873 }
1157 874
@@ -1676,28 +1393,8 @@
1676 1393 }
1677 1394 }
1678 1395 }
1679 1396 }
1680 -
1681 - /*
1682 - * The lock on the settings the shared files are built from is NOT applied
1683 - * here, and that is a decision, not an oversight. The second cross review
1684 - * of 2.11.10 raised that register_setting( 'vigilante_options', ... )
1685 - * declares this as its sanitize callback with no lock in it, so anything
1686 - * reaching options.php with that option group would write the whole
1687 - * option. The chain does not close: the plugin prints no settings_fields()
1688 - * for that group anywhere, so the nonce it would need is not obtainable,
1689 - * and the five places that do save (saving a tab, importing, a preset,
1690 - * restoring the defaults, resetting a section) all apply
1691 - * keep_locked_file_settings() themselves.
1692 - *
1693 - * Putting it here instead would be worse than the door it closes. A
1694 - * register_setting() callback runs on EVERY update_option() of this
1695 - * option, so it would also lock the writes with no user behind them: the
1696 - * expiry of Under Attack restoring what it hardened, WP-CLI and cron.
1697 - * Those are already covered by matriz-red-ajustes-compartidos.sh, which
1698 - * is where such a change would show up as a row that stopped passing.
1699 - */
1700 1397
1701 1398 return apply_filters( 'vigilante_validate_options', $validated, $input );
1702 1399 }
1703 1400