Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 85
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Brute_Force_Protection_Math_Authenticate
0.00% covered (danger)
0.00%
0 / 84
0.00% covered (danger)
0.00%
0 / 6
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 time_window
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 math_authenticate
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
110
 generate_math_page
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 process_generate_math_page
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 math_form
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3namespace Automattic\Jetpack\Waf\Brute_Force_Protection;
4
5if ( ! class_exists( 'Brute_Force_Protection_Math_Authenticate' ) ) {
6
7    /**
8     * The math captcha fallback if we can't talk to the Protect API
9     *
10     * @phan-constructor-used-for-side-effects
11     */
12    class Brute_Force_Protection_Math_Authenticate {
13
14        /**
15         * If the class is loaded.
16         *
17         * @var bool
18         */
19        public static $loaded;
20
21        /**
22         * Class constructor.
23         */
24        public function __construct() {
25
26            if ( self::$loaded ) {
27                return;
28            }
29
30            self::$loaded = 1;
31
32            add_action( 'login_form', array( $this, 'math_form' ) );
33
34            if ( isset( $_POST['jetpack_protect_process_math_form'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- No changes made, just queues the math authenticator hook.
35                add_action( 'init', array( $this, 'process_generate_math_page' ) );
36            }
37        }
38
39        /**
40         * The timeout window.
41         */
42        private static function time_window() {
43            return ceil( time() / ( MINUTE_IN_SECONDS * 2 ) );
44        }
45
46        /**
47         * Verifies that a user answered the math problem correctly while logging in.
48         *
49         * @return bool Returns true if the math is correct. Exits if not.
50         */
51        public static function math_authenticate() {
52            if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
53                $brute_force_protection = Brute_Force_Protection::instance();
54                $transient              = $brute_force_protection->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
55
56                if ( ! $transient || $transient < 1 ) {
57                    self::generate_math_page();
58                }
59                return true;
60            }
61
62            $ans         = isset( $_POST['jetpack_protect_num'] ) ? (int) $_POST['jetpack_protect_num'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- answers are salted.
63            $correct_ans = isset( $_POST['jetpack_protect_answer'] ) ? sanitize_key( $_POST['jetpack_protect_answer'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
64
65            $time_window  = self::time_window();
66            $salt         = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
67            $salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
68            $salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
69
70            if ( ! $correct_ans || ! $ans ) {
71                self::generate_math_page();
72            } elseif ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
73                wp_die(
74                    wp_kses(
75                        __(
76                            '<strong>You failed to correctly answer the math problem.</strong> This is used to combat spam when Jetpack’s Brute Force Attack Protection API is unavailable. Please use your browser’s back button to return to the login form, press the "refresh" button to generate a new math problem, and try to log in again.',
77                            'jetpack-waf'
78                        ),
79                        array( 'strong' => array() )
80                    ),
81                    '',
82                    array( 'response' => 401 )
83                );
84            } else {
85                return true;
86            }
87        }
88
89        /**
90         * Creates an interim page to collect answers to a math captcha
91         *
92         * @param string $error - the error message.
93         * @return never
94         */
95        public static function generate_math_page( $error = false ) {
96            ob_start();
97            ?>
98            <h2><?php esc_html_e( 'Please solve this math problem to prove that you are not a bot. Once you solve it, you will need to log in again.', 'jetpack-waf' ); ?></h2>
99            <?php if ( $error ) : ?>
100                <h3><?php esc_html_e( 'Your answer was incorrect, please try again.', 'jetpack-waf' ); ?></h3>
101            <?php endif ?>
102
103            <form action="<?php echo esc_url( wp_login_url() ); ?>" method="post" accept-charset="utf-8">
104                <?php self::math_form(); ?>
105                <input type="hidden" name="jetpack_protect_process_math_form" value="1" id="jetpack_protect_process_math_form" />
106                <p><input type="submit" value="<?php esc_attr_e( 'Continue &rarr;', 'jetpack-waf' ); ?>"></p>
107            </form>
108            <?php
109            $mathpage = ob_get_contents();
110            ob_end_clean();
111            wp_die(
112                $mathpage, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- content is escaped.
113                '',
114                array( 'response' => 401 )
115            );
116        }
117
118        /**
119         * Generates the math page.
120         */
121        public function process_generate_math_page() {
122            $ans         = isset( $_POST['jetpack_protect_num'] ) ? (int) $_POST['jetpack_protect_num'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- answers are salted.
123            $correct_ans = isset( $_POST['jetpack_protect_answer'] ) ? sanitize_key( $_POST['jetpack_protect_answer'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
124
125            $time_window  = self::time_window();
126            $salt         = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
127            $salted_ans_1 = hash_hmac( 'sha1', $ans, $salt . $time_window );
128            $salted_ans_2 = hash_hmac( 'sha1', $ans, $salt . ( $time_window - 1 ) );
129
130            if ( ! hash_equals( $salted_ans_1, $correct_ans ) && ! hash_equals( $salted_ans_2, $correct_ans ) ) {
131                self::generate_math_page( true );
132            } else {
133                $temp_pass = substr( hash_hmac( 'sha1', (string) wp_rand( 1, 100000000 ), get_site_option( 'jetpack_protect_key' ) ), 5, 25 );
134
135                $brute_force_protection = Brute_Force_Protection::instance();
136                $brute_force_protection->set_transient( 'jpp_math_pass_' . $temp_pass, 3, DAY_IN_SECONDS );
137                setcookie( 'jpp_math_pass', $temp_pass, time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );
138                remove_action( 'login_form', array( $this, 'math_form' ) );
139                return true;
140            }
141        }
142
143        /**
144         * Requires a user to solve a simple equation. Added to any WordPress login form.
145         *
146         * @return VOID outputs html
147         */
148        public static function math_form() {
149            // Check if jpp_math_pass cookie is set and it matches valid transient.
150            if ( isset( $_COOKIE['jpp_math_pass'] ) ) {
151                $brute_force_protection = Brute_Force_Protection::instance();
152                $transient              = $brute_force_protection->get_transient( 'jpp_math_pass_' . sanitize_key( $_COOKIE['jpp_math_pass'] ) );
153
154                if ( $transient && $transient > 0 ) {
155                    return '';
156                }
157            }
158
159            $num1 = wp_rand( 0, 10 );
160            $num2 = wp_rand( 1, 10 );
161            $ans  = $num1 + $num2;
162
163            $time_window = self::time_window();
164            $salt        = get_site_option( 'jetpack_protect_key' ) . '|' . get_site_option( 'admin_email' ) . '|';
165            $salted_ans  = hash_hmac( 'sha1', (string) $ans, $salt . $time_window );
166            ?>
167            <div style="margin: 5px 0 20px;">
168                <p style="font-size: 14px;">
169                    <?php esc_html_e( 'Prove your humanity', 'jetpack-waf' ); ?>
170                </p>
171                <br/>
172                <label for="jetpack_protect_answer" style="vertical-align:super;">
173                    <?php echo esc_html( "$num1 &nbsp; + &nbsp; $num2 &nbsp; = &nbsp;" ); ?>
174                </label>
175                <input type="number" id="jetpack_protect_answer" name="jetpack_protect_num" value="" size="2" style="width:50px;height:25px;vertical-align:middle;font-size:13px;" class="input" />
176                <input type="hidden" name="jetpack_protect_answer" value="<?php echo esc_attr( $salted_ans ); ?>" />
177            </div>
178            <?php
179        }
180    }
181}