1717 */
1818trait Cloud_Auth_Callback {
1919
20+ /**
21+ * Successfully authorized REST requests retained for WordPress's repeated permission check.
22+ *
23+ * @var array<string,array<string,mixed>>
24+ */
25+ private $ _validated_callback_requests = [];
26+
2027 /**
2128 * Encrypt data for cloud req
2229 *
@@ -95,8 +102,9 @@ private function _local_sign_sk() {
95102 * @return array<int,string> Binary public keys.
96103 */
97104 private function _trusted_server_pks ( $ from_wpapi = false , $ key_id = '' ) {
98- $ source = $ from_wpapi ? 'wpapi ' : 'qc ' ;
99- $ ring = isset ( self ::SERVER_SIGN_KEYS [ $ source ] ) ? self ::SERVER_SIGN_KEYS [ $ source ] : [];
105+ $ environment = false !== strpos ( $ this ->_cloud_server , 'preview. ' ) ? 'preview ' : 'prod ' ;
106+ $ source = $ from_wpapi ? 'wpapi ' : 'qc ' ;
107+ $ ring = isset ( self ::SERVER_SIGN_KEYS [ $ environment ][ $ source ] ) ? self ::SERVER_SIGN_KEYS [ $ environment ][ $ source ] : [];
100108 if ( '' !== $ key_id ) {
101109 $ ring = isset ( $ ring [ $ key_id ] ) ? [ $ ring [ $ key_id ] ] : [];
102110 }
@@ -328,8 +336,7 @@ private function _verify_signed( $signed_bytes, $signature_b64, $qc_ts, $qc_nonc
328336 */
329337 public function validate_signed_callback ( $ request , $ action ) {
330338 if (
331- ! is_object ( $ request ) || ! method_exists ( $ request , 'get_body ' ) || ! method_exists ( $ request , 'get_header ' ) ||
332- empty ( $ this ->_summary ['pk_b64 ' ] ) || ! is_string ( $ this ->_summary ['pk_b64 ' ] ) || ! is_string ( $ action )
339+ ! is_object ( $ request ) || ! method_exists ( $ request , 'get_body ' ) || ! method_exists ( $ request , 'get_header ' ) || ! is_string ( $ action )
333340 ) {
334341 return $ this ->_callback_error ( self ::CALLBACK_ERR_REQUEST , 'Invalid signed callback request. ' );
335342 }
@@ -368,7 +375,7 @@ public function validate_signed_callback( $request, $action ) {
368375 ! is_array ( $ payload ) ||
369376 ! isset ( $ payload ['qc_sig_v ' ] ) || ! is_int ( $ payload ['qc_sig_v ' ] ) || self ::SIGN_VERSION !== $ payload ['qc_sig_v ' ] ||
370377 empty ( $ payload ['qc_action ' ] ) || ! is_string ( $ payload ['qc_action ' ] ) ||
371- empty ( $ payload ['wp_pk_b64 ' ] ) || ! is_string ( $ payload ['wp_pk_b64 ' ] ) ||
378+ ! isset ( $ payload ['wp_pk_sha256 ' ] ) || ! is_string ( $ payload ['wp_pk_sha256 ' ] ) || ! preg_match ( ' /^[a-f0-9]{64}$/D ' , $ payload [ ' wp_pk_sha256 ' ] ) ||
372379 ! isset ( $ payload ['qc_ts ' ] ) || ! is_string ( $ payload ['qc_ts ' ] ) ||
373380 ! isset ( $ payload ['qc_nonce ' ] ) || ! is_string ( $ payload ['qc_nonce ' ] ) ||
374381 ( isset ( $ payload ['qc_key_id ' ] ) && ! is_string ( $ payload ['qc_key_id ' ] ) )
@@ -379,19 +386,40 @@ public function validate_signed_callback( $request, $action ) {
379386 if ( ! hash_equals ( $ action , $ payload ['qc_action ' ] ) ) {
380387 return $ this ->_callback_error ( self ::CALLBACK_ERR_ACTION , 'Callback action does not match its route. ' );
381388 }
382- if ( ! hash_equals ( (string ) $ this ->_summary ['pk_b64 ' ], $ payload ['wp_pk_b64 ' ] ) ) {
383- return $ this ->_callback_error ( self ::CALLBACK_ERR_SITE , 'Callback site key does not match this site. ' );
389+
390+ $ request_key = spl_object_hash ( $ request ) . ': ' . hash ( 'sha256 ' , $ action );
391+ $ fingerprint = hash ( 'sha256 ' , $ body ) . hash ( 'sha256 ' , $ content_type ) . hash ( 'sha256 ' , $ content_encoding ) . hash ( 'sha256 ' , $ signature_b64 );
392+ if (
393+ isset ( $ this ->_validated_callback_requests [ $ request_key ] ) &&
394+ $ this ->_validated_callback_requests [ $ request_key ]['request ' ] === $ request &&
395+ hash_equals ( $ this ->_validated_callback_requests [ $ request_key ]['fingerprint ' ], $ fingerprint )
396+ ) {
397+ return true ;
384398 }
385399
386- $ key_id = isset ( $ payload ['qc_key_id ' ] ) ? $ payload ['qc_key_id ' ] : '' ;
387- return $ this ->_verify_signed (
400+ if ( empty ( $ this ->_summary ['pk_b64 ' ] ) || ! is_string ( $ this ->_summary ['pk_b64 ' ] ) ) {
401+ return $ this ->_callback_error ( self ::CALLBACK_ERR_REQUEST , 'Invalid signed callback request. ' );
402+ }
403+ if ( ! hash_equals ( hash ( 'sha256 ' , (string ) $ this ->_summary ['pk_b64 ' ] ), $ payload ['wp_pk_sha256 ' ] ) ) {
404+ return $ this ->_callback_error ( self ::CALLBACK_ERR_SITE , 'Callback site-key digest does not match this site. ' );
405+ }
406+
407+ $ key_id = isset ( $ payload ['qc_key_id ' ] ) ? $ payload ['qc_key_id ' ] : '' ;
408+ $ verified = $ this ->_verify_signed (
388409 $ body ,
389410 trim ( $ signature_b64 ),
390411 $ payload ['qc_ts ' ],
391412 $ payload ['qc_nonce ' ],
392413 self ::SIGN_ACTION_NOTIFY_IMG === $ action ,
393414 $ key_id
394415 );
416+ if ( true === $ verified ) {
417+ $ this ->_validated_callback_requests [ $ request_key ] = [
418+ 'request ' => $ request ,
419+ 'fingerprint ' => $ fingerprint ,
420+ ];
421+ }
422+ return $ verified ;
395423 }
396424
397425 /**
0 commit comments