PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.20
ووسلام – همگام سازی ووکامرس و باسلام v1.10.20
1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
← All changes | includes/Admin/Settings/OAuthManager.php +157 -1 1.8.51.10.20 View file →
@@ -8,8 +8,152 @@
8 8 defined('ABSPATH') || exit;
9 9
10 10 class OAuthManager
11 11 {
12 + /** Signed, browser-bound proof that an administrator started OAuth. */
13 + const OAUTH_STATE_COOKIE = 'sync_basalam_oauth_state';
14 +
15 + /** Lifetime of a pending OAuth authorization — the SSO round-trip window. */
16 + const OAUTH_STATE_TTL = 600; // 10 * MINUTE_IN_SECONDS
17 +
18 + /**
19 + * Remember that the current admin has just started an OAuth authorization.
20 + *
21 + * This is called only from the nonce-protected initiation flow, so the
22 + * cookie it stores cannot be planted by a forged cross-site request. The
23 + * callback later requires (and consumes) this cookie, which is what turns
24 + * the token-saving callback from "always forgeable" into "only valid for a
25 + * flow this admin actually started".
26 + *
27 + * The proof deliberately lives in a signed HttpOnly cookie instead of a
28 + * WordPress transient. Sites with a persistent object-cache drop-in route
29 + * transients through Redis/Memcached, where a failed write, eviction, or
30 + * cache flush during the OAuth round trip would otherwise invalidate a
31 + * legitimate callback.
32 + */
33 + public static function issueOauthState()
34 + {
35 + $userId = get_current_user_id();
36 + if ($userId <= 0) return false;
37 +
38 + $state = wp_generate_password(64, false);
39 + $expiresAt = time() + self::OAUTH_STATE_TTL;
40 + $value = self::buildOauthStateCookieValue($state, $userId, $expiresAt);
41 +
42 + if (! self::writeOauthStateCookie($value, $expiresAt)) return false;
43 +
44 + return $state;
45 + }
46 +
47 + /**
48 + * Validate and consume the pending OAuth authorization for the current user.
49 + *
50 + * Single use: the cookie is deleted whether or not it was valid, so a
51 + * replayed or forged callback cannot reuse it.
52 + */
53 + private static function verifyOauthState()
54 + {
55 + $value = isset($_COOKIE[self::OAUTH_STATE_COOKIE])
56 + ? (string) wp_unslash($_COOKIE[self::OAUTH_STATE_COOKIE])
57 + : '';
58 +
59 + self::clearOauthStateCookie();
60 +
61 + return self::isOauthStateCookieValid(
62 + $value,
63 + get_current_user_id(),
64 + time()
65 + );
66 + }
67 +
68 + private static function buildOauthStateCookieValue($state, $userId, $expiresAt)
69 + {
70 + $payload = json_encode([
71 + 'state' => (string) $state,
72 + 'user_id' => (int) $userId,
73 + 'expires_at' => (int) $expiresAt,
74 + ]);
75 +
76 + if (! is_string($payload)) return '';
77 +
78 + $encodedPayload = rtrim(strtr(base64_encode($payload), '+/', '-_'), '=');
79 + $signature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
80 +
81 + return $encodedPayload . '.' . $signature;
82 + }
83 +
84 + private static function isOauthStateCookieValid($value, $userId, $now)
85 + {
86 + if (! is_string($value) || $value === '' || (int) $userId <= 0) return false;
87 +
88 + $parts = explode('.', $value, 2);
89 + if (count($parts) !== 2) return false;
90 +
91 + [$encodedPayload, $signature] = $parts;
92 + $expectedSignature = hash_hmac('sha256', $encodedPayload, wp_salt('auth'));
93 +
94 + if (! hash_equals($expectedSignature, $signature)) return false;
95 +
96 + $padding = strlen($encodedPayload) % 4;
97 + if ($padding !== 0) $encodedPayload .= str_repeat('=', 4 - $padding);
98 +
99 + $payload = base64_decode(strtr($encodedPayload, '-_', '+/'), true);
100 + $data = is_string($payload) ? json_decode($payload, true) : null;
101 +
102 + if (! is_array($data)) return false;
103 +
104 + return ! empty($data['state'])
105 + && (int) ($data['user_id'] ?? 0) === (int) $userId
106 + && (int) ($data['expires_at'] ?? 0) >= (int) $now;
107 + }
108 +
109 + private static function writeOauthStateCookie($value, $expiresAt)
110 + {
111 + if (! is_string($value) || $value === '' || headers_sent()) return false;
112 +
113 + $written = setcookie(self::OAUTH_STATE_COOKIE, $value, [
114 + 'expires' => (int) $expiresAt,
115 + 'path' => self::oauthStateCookiePath(),
116 + 'domain' => self::oauthStateCookieDomain(),
117 + 'secure' => is_ssl(),
118 + 'httponly' => true,
119 + 'samesite' => 'Lax',
120 + ]);
121 +
122 + // Keep the current request internally consistent for callers and tests.
123 + if ($written) $_COOKIE[self::OAUTH_STATE_COOKIE] = $value;
124 +
125 + return $written;
126 + }
127 +
128 + private static function clearOauthStateCookie()
129 + {
130 + unset($_COOKIE[self::OAUTH_STATE_COOKIE]);
131 +
132 + if (headers_sent()) return;
133 +
134 + setcookie(self::OAUTH_STATE_COOKIE, '', [
135 + 'expires' => time() - HOUR_IN_SECONDS,
136 + 'path' => self::oauthStateCookiePath(),
137 + 'domain' => self::oauthStateCookieDomain(),
138 + 'secure' => is_ssl(),
139 + 'httponly' => true,
140 + 'samesite' => 'Lax',
141 + ]);
142 + }
143 +
144 + private static function oauthStateCookiePath()
145 + {
146 + return defined('ADMIN_COOKIE_PATH') && ADMIN_COOKIE_PATH
147 + ? ADMIN_COOKIE_PATH
148 + : '/wp-admin';
149 + }
150 +
151 + private static function oauthStateCookieDomain()
152 + {
153 + return defined('COOKIE_DOMAIN') ? (string) COOKIE_DOMAIN : '';
154 + }
155 +
12 156 public function getOauthData()
13 157 {
14 158 $oauthDataUrl = apply_filters('sync_basalam_oauth_data_url', Endpoints::HAMSALAM_OAUTH_DATA);
15 159 $defaultClientId = apply_filters('sync_basalam_oauth_default_client_id', 779);
@@ -32,8 +176,20 @@
32 176 }
33 177
34 178 public static function saveOauthData()
35 179 {
180 + // CSRF protection: this callback performs a state-changing write from a
181 + // plain GET, so it must be tied to an OAuth flow the current admin
182 + // actually initiated. Without this an attacker could lure a logged-in
183 + // admin to the callback URL and overwrite the stored Basalam credentials.
184 + if (! current_user_can('manage_options') || ! self::verifyOauthState()) {
185 + wp_die(
186 + esc_html__('درخواست نامعتبر است.', 'sync-basalam'),
187 + esc_html__('خطای امنیتی', 'sync-basalam'),
188 + ['response' => 403]
189 + );
190 + }
191 +
36 192 $isVendor = isset($_GET['is_vendor']) ? sanitize_text_field(wp_unslash($_GET['is_vendor'])) : true;
37 193 $vendorId = isset($_GET['vendor_id']) ? sanitize_text_field(intval($_GET['vendor_id'])) : null;
38 194 $hamsalamToken = isset($_GET['hamsalam_token']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_token'])) : null;
39 195 $hamsalamBusinessId = isset($_GET['hamsalam_business_id']) ? sanitize_text_field(wp_unslash($_GET['hamsalam_business_id'])) : null;
@@ -72,9 +228,9 @@
72 228 {
73 229 $oauthData = $this->getOauthData();
74 230 $siteUrl = get_site_url();
75 231
76 - $scopes = apply_filters('sync_basalam_oauth_scopes', "vendor.product.write vendor.parcel.write customer.profile.read vendor.profile.read vendor.parcel.read vendor.profile.write customer.chat.read customer.chat.write");
232 + $scopes = apply_filters('sync_basalam_oauth_scopes', "vendor.product.write vendor.parcel.write customer.profile.read vendor.profile.read vendor.parcel.read vendor.profile.write customer.chat.read customer.chat.write customer.identity.read");
77 233
78 234 return [
79 235 'redirect_uri' => $oauthData['redirect_uri'],
80 236 'url_req_token' => Endpoints::oauthLoginUrl(