PluginProbe ʕ •ᴥ•ʔ
OttoKit: All-in-One Automation Platform / 1.1.36
OttoKit: All-in-One Automation Platform v1.1.36
1.1.36 1.1.35 1.1.34 1.1.33 1.1.32 1.1.31 1.1.30 1.1.29 1.1.28 1.1.27 1.1.9 trunk 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 1.0.18 1.0.19 1.0.20 1.0.21 1.0.22 1.0.23 1.0.24 1.0.25 1.0.26 1.0.27 1.0.28 1.0.29 1.0.30 1.0.31 1.0.32 1.0.33 1.0.34 1.0.35 1.0.36 1.0.37 1.0.38 1.0.39 1.0.40 1.0.41 1.0.42 1.0.43 1.0.44 1.0.45 1.0.46 1.0.47 1.0.48 1.0.49 1.0.50 1.0.51 1.0.52 1.0.53 1.0.54 1.0.55 1.0.56 1.0.57 1.0.58 1.0.59 1.0.60 1.0.61 1.0.62 1.0.63 1.0.64 1.0.65 1.0.66 1.0.67 1.0.68 1.0.69 1.0.7 1.0.70 1.0.71 1.0.72 1.0.73 1.0.74 1.0.75 1.0.76 1.0.77 1.0.78 1.0.79 1.0.8 1.0.80 1.0.81 1.0.82 1.0.83 1.0.84 1.0.85 1.0.86 1.0.87 1.0.88 1.0.89 1.0.9 1.0.90 1.1.0 1.1.1 1.1.10 1.1.11 1.1.12 1.1.13 1.1.14 1.1.15 1.1.16 1.1.17 1.1.18 1.1.19 1.1.2 1.1.20 1.1.21 1.1.22 1.1.23 1.1.24 1.1.25 1.1.26 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8
suretriggers / src / Integrations / sureforms / actions / sureforms-send-data.php
suretriggers / src / Integrations / sureforms / actions Last commit date
sureforms-send-data.php 1 day ago
sureforms-send-data.php
294 lines
1 <?php
2 /**
3 * SureFormsSendData.
4 * php version 5.6
5 *
6 * @category SureFormsSendData
7 * @package SureTriggers
8 * @author BSF <username@example.com>
9 * @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
10 * @link https://www.brainstormforce.com/
11 * @since 1.0.0
12 */
13
14 namespace SureTriggers\Integrations\SureForms\Actions;
15
16 use SureTriggers\Integrations\AutomateAction;
17 use SureTriggers\Traits\SingletonLoader;
18
19 /**
20 * SureFormsSendData
21 *
22 * @category SureFormsSendData
23 * @package SureTriggers
24 * @author BSF <username@example.com>
25 * @license https://www.gnu.org/licenses/gpl-3.0.html GPLv3
26 * @link https://www.brainstormforce.com/
27 * @since 1.0.0
28 */
29 class SureFormsSendData extends AutomateAction {
30
31 /**
32 * Integration type.
33 *
34 * @var string
35 */
36 public $integration = 'SureForms';
37
38 /**
39 * Action name.
40 *
41 * @var string
42 */
43 public $action = 'sureforms_send_data';
44
45 use SingletonLoader;
46
47 /**
48 * Register a action.
49 *
50 * @param array $actions actions.
51 * @return array
52 */
53 public function register( $actions ) {
54 $actions[ $this->integration ][ $this->action ] = [
55 'label' => __( 'Send Data', 'suretriggers' ),
56 'action' => $this->action,
57 'function' => [ $this, 'action_listener' ],
58 ];
59 return $actions;
60 }
61
62 /**
63 * Action listener.
64 *
65 * @param int $user_id user_id.
66 * @param int $automation_id automation_id.
67 * @param array $fields fields.
68 * @param array $selected_options selectedOptions.
69 * @psalm-suppress UndefinedMethod
70 *
71 * @throws \Exception Exception.
72 *
73 * @return array|mixed
74 */
75 public function _action_listener( $user_id, $automation_id, $fields, $selected_options ) {
76 $endpoint_url = isset( $selected_options['endpoint_url'] ) ? esc_url( $selected_options['endpoint_url'] ) : '';
77 $sf_data = isset( $selected_options['sf_data_body'] ) ? $selected_options['sf_data_body'] : '';
78 $file_attachment = isset( $selected_options['sf_attachment'] ) ? $selected_options['sf_attachment'] : '';
79
80 // Handling SSRF Attack.
81 $host = wp_parse_url( $endpoint_url, PHP_URL_HOST );
82
83 if ( empty( $host ) || $this->is_host_blocked( $host ) ) {
84 throw new \Exception( 'Access blocked.' );
85 }
86
87 $form_data = [
88 'body' => $sf_data,
89 'attachment' => $file_attachment,
90 ];
91 $json_body = wp_json_encode( $form_data );
92 if ( false === $json_body ) {
93 throw new \Exception( 'Failed to encode form data to JSON.' );
94 }
95
96 $args = [
97 'method' => 'POST',
98 'headers' => [
99 'Content-Type' => 'application/json',
100 'User-Agent' => 'SureTriggers',
101 ],
102 'sslverify' => true,
103 'timeout' => 30, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout
104 'body' => $json_body,
105 ];
106
107 if ( null === $endpoint_url ) {
108 return [];
109 }
110 // Send the HTTP request based on the method. wp_safe_remote_request() re-validates
111 // the resolved host (including on redirects) against internal/reserved IP ranges.
112 $response = wp_safe_remote_request( $endpoint_url, $args );
113 if ( is_wp_error( $response ) ) {
114 $error_message = $response->get_error_message();
115 if ( ! empty( $selected_options['test_action'] ) ) {
116 return [
117 'success' => false,
118 'message' => 'Error: ' . $error_message,
119 ];
120 }
121 throw new \Exception( 'Request failed: ' . $error_message );
122 }
123
124 // Check for successful HTTP status codes (200, 201, 204).
125 $status_code = wp_remote_retrieve_response_code( $response );
126 if ( ! in_array( $status_code, [ 200, 201, 204 ], true ) ) {
127 $error = 'Failed to communicate with the API: ' . $endpoint_url;
128 if ( ! empty( $selected_options['test_action'] ) ) {
129 return [
130 'success' => false,
131 'message' => $error,
132 ];
133 }
134 throw new \Exception( 'API request failed: ' . wp_remote_retrieve_body( $response ) );
135 }
136
137 $result = json_decode( wp_remote_retrieve_body( $response ), true );
138 if ( json_last_error() !== JSON_ERROR_NONE ) {
139 $result = [ 'response' => wp_remote_retrieve_body( $response ) ];
140 }
141 return $result;
142 }
143
144 /**
145 * Determine whether a host resolves to a private or reserved IP address.
146 *
147 * Resolves the host name (normalizing decimal/octal/hex IP literals first)
148 * and validates every resolved address against private and reserved IP
149 * ranges, rather than string-comparing the host against CIDR literals.
150 *
151 * @param string $host Host name or IP literal parsed from the endpoint URL.
152 * @return bool
153 */
154 private function is_host_blocked( $host ) {
155 $host = strtolower( trim( $host, '[]' ) );
156
157 if ( 'localhost' === $host ) {
158 return true;
159 }
160
161 $ips = $this->resolve_host_ips( $host );
162
163 if ( empty( $ips ) ) {
164 // Host could not be resolved to any address - fail securely.
165 return true;
166 }
167
168 foreach ( $ips as $ip ) {
169 if ( $this->is_blocked_ip( $ip ) ) {
170 return true;
171 }
172
173 // Unwrap IPv4-mapped IPv6 addresses (e.g. ::ffff:127.0.0.1) - PHP's
174 // range flags don't evaluate the embedded IPv4 address on their own.
175 $mapped_ipv4 = $this->extract_mapped_ipv4( $ip );
176 if ( null !== $mapped_ipv4 && $this->is_blocked_ip( $mapped_ipv4 ) ) {
177 return true;
178 }
179 }
180
181 return false;
182 }
183
184 /**
185 * Check a single IP address against private and reserved IP ranges.
186 *
187 * @param string $ip IP address.
188 * @return bool
189 */
190 private function is_blocked_ip( $ip ) {
191 return false === filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE );
192 }
193
194 /**
195 * Extract the embedded IPv4 address from an IPv4-mapped IPv6 address.
196 *
197 * @param string $ip IP address.
198 * @return string|null
199 */
200 private function extract_mapped_ipv4( $ip ) {
201 $binary = @inet_pton( $ip ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Generic.PHP.NoSilencedErrors.Discouraged
202 if ( false === $binary || 16 !== strlen( $binary ) ) {
203 return null;
204 }
205
206 if ( "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff" !== substr( $binary, 0, 12 ) ) {
207 return null;
208 }
209
210 $ipv4 = inet_ntop( substr( $binary, 12, 4 ) );
211 return false !== $ipv4 ? $ipv4 : null;
212 }
213
214 /**
215 * Resolve a host name (or IP literal) to its IP address(es).
216 *
217 * @param string $host Host name or IP literal.
218 * @return string[]
219 */
220 private function resolve_host_ips( $host ) {
221 $normalized = $this->normalize_ip_literal( $host );
222
223 if ( null !== $normalized ) {
224 return [ $normalized ];
225 }
226
227 if ( false !== filter_var( $host, FILTER_VALIDATE_IP ) ) {
228 return [ $host ];
229 }
230
231 $ips = gethostbynamel( $host );
232 $ips = false !== $ips ? $ips : [];
233
234 if ( function_exists( 'dns_get_record' ) ) {
235 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged, Generic.PHP.NoSilencedErrors.Discouraged
236 $records = @dns_get_record( $host, DNS_AAAA );
237 if ( is_array( $records ) ) {
238 foreach ( $records as $record ) {
239 if ( ! empty( $record['ipv6'] ) ) {
240 $ips[] = $record['ipv6'];
241 }
242 }
243 }
244 }
245
246 return $ips;
247 }
248
249 /**
250 * Normalize decimal, octal, and hexadecimal IPv4 literals (e.g. `2130706433`
251 * or `0177.0.0.1`) to dotted-quad form so they can't slip past validation
252 * in a format that never gets resolved via DNS.
253 *
254 * @param string $host Host name or IP literal.
255 * @return string|null
256 */
257 private function normalize_ip_literal( $host ) {
258 if ( ctype_digit( $host ) && strlen( $host ) <= 10 ) {
259 $decimal = (float) $host;
260 if ( $decimal >= 0 && $decimal <= 4294967295 ) {
261 $ip = long2ip( (int) $decimal );
262 return false !== $ip ? $ip : null;
263 }
264 }
265
266 $parts = explode( '.', $host );
267 if ( 4 !== count( $parts ) ) {
268 return null;
269 }
270
271 $octets = [];
272 foreach ( $parts as $part ) {
273 if ( ! preg_match( '/^(0x[0-9a-f]+|0[0-7]*|[1-9][0-9]*)$/i', $part ) ) {
274 return null;
275 }
276 if ( 0 === strncasecmp( $part, '0x', 2 ) ) {
277 $octet = hexdec( $part );
278 } elseif ( strlen( $part ) > 1 && '0' === $part[0] ) {
279 $octet = octdec( $part );
280 } else {
281 $octet = (int) $part;
282 }
283 if ( $octet < 0 || $octet > 255 ) {
284 return null;
285 }
286 $octets[] = $octet;
287 }
288
289 return implode( '.', $octets );
290 }
291 }
292
293 SureFormsSendData::get_instance();
294