PluginProbe
DoLogin Security / trunk
DoLogin Security vtrunk
5.0.10 4.8.3 trunk 1.0 1.1 1.1.1 1.2 1.2.1 1.2.2 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.5 1.6 All 64 releases
dologin / src / captcha.cls.php

captcha.cls.php in DoLogin Security trunk, at src/captcha.cls.php

148 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Captcha class
4 *
5 * @since 1.6
6 * @package dologin
7 */
8
9 namespace dologin;
10
11 defined( 'WPINC' ) || exit;
12
13 class Captcha extends Instance {
14
15 /**
16 * Display Cloudflare Turnstile
17 *
18 * @since 1.6
19 */
20 public function show() {
21 // Cloudflare Turnstile must load its api.js from Cloudflare's domain; it cannot be self-hosted.
22 // phpcs:ignore PluginCheck.CodeAnalysis.EnqueuedResourceOffloading.OffloadedContent
23 wp_register_script( 'dologin_cf_api', 'https://challenges.cloudflare.com/turnstile/v0/api.js', array(), Core::VER, true );
24 wp_enqueue_script( 'dologin_cf_api' );
25
26 echo '<div class="cf-turnstile" data-sitekey="' . esc_attr( Conf::val( 'cf_pub_key' ) ) . '"></div>';
27 }
28
29 /**
30 * Validate Cloudflare Turnstile
31 *
32 * @since 1.6
33 */
34 public function authenticate() {
35 // This runs on the public login form / REST 2-step and is authenticated by the Turnstile token itself, not a WP nonce.
36 // phpcs:ignore WordPress.Security.NonceVerification.Missing
37 if ( empty( $_POST['cf-turnstile-response'] ) || ! is_string( $_POST['cf-turnstile-response'] ) ) {
38 throw new \Exception( 'captcha_missing' );
39 }
40
41 // Check if stored token matches, then bypass.
42 if ( $this->_validate_token() ) {
43 defined( 'debug' ) && debug( '�
44 bypassed, token matched' );
45 return;
46 }
47
48 // phpcs:ignore WordPress.Security.NonceVerification.Missing
49 $cf_response = sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) );
50
51 // phpcs:ignore PluginCheck.CodeAnalysis.Offloading.OffloadedContent -- Cloudflare Turnstile verification endpoint; required by the captcha feature and cannot be self-hosted.
52 $url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
53 $data = array(
54 'secret' => Conf::val( 'cf_priv_key' ),
55 'response' => $cf_response,
56 'remoteip' => IP::me(),
57 );
58
59 $res = wp_safe_remote_post(
60 $url,
61 array(
62 'body' => $data,
63 'timeout' => 10,
64 'redirection' => 0,
65 'limit_response_size' => 32768,
66 'sslverify' => true,
67 )
68 );
69
70 if ( is_wp_error( $res ) ) {
71 defined( 'debug' ) && debug( '❌ Turnstile transport error: ' . $res->get_error_message() );
72 throw new \Exception( 'captcha_transport_error' );
73 }
74
75 if ( 200 !== (int) wp_remote_retrieve_response_code( $res ) ) {
76 defined( 'debug' ) && debug( '❌ Turnstile service HTTP status: ' . (int) wp_remote_retrieve_response_code( $res ) );
77 throw new \Exception( 'captcha_service_error' );
78 }
79
80 $res = json_decode( wp_remote_retrieve_body( $res ), true );
81 defined( 'debug' ) && debug( 'Turnstile verification response:', $res );
82
83 if ( empty( $res['success'] ) ) {
84 $err_code = ! empty( $res['error-codes'][0] ) && is_string( $res['error-codes'][0] ) ? sanitize_key( $res['error-codes'][0] ) : 'error';
85 $err_code = $err_code ? $err_code : 'error';
86
87 throw new \Exception( $err_code );
88 }
89
90 // Mark this session as trusted, to prevent duplicate check when submitting 2FA.
91 $this->_store_token();
92
93 defined( 'debug' ) && debug( '�
94 passed' );
95 }
96
97 /**
98 * Store token for 2nd step verification use
99 *
100 * @since 4.2
101 */
102 private function _store_token() {
103 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
104 $response = sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) );
105 $expiration = 5 * MINUTE_IN_SECONDS;
106 set_transient( $this->_generate_token_tag( $response ), true, $expiration );
107 }
108
109 /**
110 * Generate the token tag to use in storage
111 *
112 * @since 4.2
113 */
114 private function _generate_token_tag( $response ) {
115 $tag = IP::me();
116 // phpcs:ignore WordPress.Security.NonceVerification.Missing
117 if ( ! empty( $_POST['log'] ) ) {
118 // phpcs:ignore WordPress.Security.NonceVerification.Missing
119 $tag = sanitize_text_field( wp_unslash( $_POST['log'] ) );
120 }
121 // phpcs:ignore WordPress.Security.NonceVerification.Missing
122 if ( ! empty( $_POST['user_login'] ) ) {
123 // phpcs:ignore WordPress.Security.NonceVerification.Missing
124 $tag = sanitize_text_field( wp_unslash( $_POST['user_login'] ) );
125 }
126 return 'dologin_tmp_data_' . hash( 'sha256', $tag . '|' . IP::me() . '|' . (string) $response );
127 }
128
129 /**
130 * One time token validation and delete
131 *
132 * @since 4.2
133 */
134 private function _validate_token() {
135 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated
136 $response = sanitize_text_field( wp_unslash( $_POST['cf-turnstile-response'] ) );
137 $transient_key = $this->_generate_token_tag( $response );
138 $stored_token = get_transient( $transient_key );
139
140 if ( true === $stored_token || '1' === $stored_token ) {
141 delete_transient( $transient_key );
142 return true;
143 }
144
145 return false;
146 }
147 }
148