get_route(), $request->get_method() ) ) { return $result; } return new \WP_Error( 'cortexcommerce_key_out_of_scope', __( 'The CortexCommerce key can only read products and update a product, and use the CortexCommerce endpoints.', 'cortexcommerce' ), array( 'status' => 403 ) ); } /** * Whether this request presents the key the plugin minted, in any form WooCommerce accepts. */ public static function presented_key_is_ours(): bool { $presented = self::presented_consumer_keys(); if ( array() === $presented || ! function_exists( 'wc_api_hash' ) ) { return false; } foreach ( array( Options::key_id(), Options::pending_key_id() ) as $key_id ) { $ours = self::key_hash( $key_id ); if ( '' === $ours ) { continue; } foreach ( $presented as $consumer_key ) { // Exactly WooCommerce's own lookup: WC_REST_Authentication::get_user_data_by_consumer_key(). if ( hash_equals( $ours, wc_api_hash( sanitize_text_field( $consumer_key ) ) ) ) { return true; } } } return false; } /** * Every consumer key the request presents, in every form WooCommerce reads one from: the consumer_key query * arg, HTTP basic auth, and OAuth 1.0a (query, body or Authorization header, parsed by WooCommerce itself). * A superset is safe: these are only ever hashed and compared with our key, never stored or output. * * @return array */ private static function presented_consumer_keys(): array { // phpcs:disable WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput -- credentials are only hashed and compared (after WooCommerce's own sanitize_text_field), never stored or output. $candidates = array( isset( $_GET['consumer_key'] ) ? $_GET['consumer_key'] : null, isset( $_SERVER['PHP_AUTH_USER'] ) ? $_SERVER['PHP_AUTH_USER'] : null, ); // phpcs:enable $wc_auth = self::woocommerce_authentication(); if ( null !== $wc_auth ) { $oauth = $wc_auth->get_oauth_parameters(); if ( is_array( $oauth ) && isset( $oauth['oauth_consumer_key'] ) ) { $candidates[] = $oauth['oauth_consumer_key']; } $header = (string) $wc_auth->get_authorization_header(); if ( 1 === preg_match( '/^\s*Basic\s+(\S+)/i', $header, $match ) ) { // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- decoding HTTP Basic credentials. $decoded = base64_decode( $match[1], true ); if ( false !== $decoded && false !== strpos( $decoded, ':' ) ) { $candidates[] = (string) strstr( $decoded, ':', true ); } } } $keys = array(); foreach ( $candidates as $candidate ) { if ( is_string( $candidate ) && '' !== $candidate ) { $keys[] = $candidate; $keys[] = wp_unslash( $candidate ); } } return array_values( array_unique( $keys ) ); } /** * WooCommerce's own authenticator, used only for its public, side-effect-free parsers. Built without its * constructor, which would register a second copy of WooCommerce's authentication filters. */ private static function woocommerce_authentication(): ?\WC_REST_Authentication { static $instance = null; if ( null === $instance && class_exists( 'WC_REST_Authentication' ) ) { $instance = ( new \ReflectionClass( 'WC_REST_Authentication' ) )->newInstanceWithoutConstructor(); } return $instance; } /** * The stored hash of a key's consumer key, or "" when there is no such key. Looked up once per key per request. */ private static function key_hash( int $key_id ): string { static $hashes = array(); if ( $key_id <= 0 ) { return ''; } if ( ! isset( $hashes[ $key_id ] ) ) { global $wpdb; // By id alone: an admin may rename the key in WooCommerce's settings, and it must stay in scope. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- WooCommerce's keys table has no API; runs only when a request presents a consumer key, then memoised. $hash = $wpdb->get_var( $wpdb->prepare( "SELECT consumer_key FROM {$wpdb->prefix}woocommerce_api_keys WHERE key_id = %d", $key_id ) ); $hashes[ $key_id ] = is_string( $hash ) ? $hash : ''; } return $hashes[ $key_id ]; } }