PluginProbe
Fluent Support – Helpdesk & Customer Support Ticket System / 2.4.5
Fluent Support – Helpdesk & Customer Support Ticket System v2.4.5
2.4.5 2.4.0 2.3.2 2.3.1 2.3.0 2.2.1 2.2.0 trunk 1.10.0 1.10.1 1.10.2 1.10.3 1.10.4 1.10.5 1.4.0 1.4.1 1.4.2 1.4.5 1.4.6 1.4.7 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 All 69 releases
← All changes | app/Services/Tickets/Importer/Common.php +126 -16 2.3.0 → 2.4.5 View file →
@@ -32,34 +32,144 @@
32 32 'person_type' => $type
33 33 ];
34 34 }
35 35
36 + // Reduce a remote-supplied attachment name to a safe, traversal-free basename.
37 + public static function sanitizeAttachmentFilename($fileName)
38 + {
39 + $fileName = wp_basename((string) $fileName);
40 + $fileName = sanitize_file_name($fileName);
41 + $fileName = str_replace("\0", '', $fileName);
42 + $fileName = ltrim($fileName, '.');
43 +
44 + if ($fileName === '') {
45 + $fileName = 'attachment-' . wp_generate_password(8, false, false);
46 + }
47 +
48 + return $fileName;
49 + }
50 +
51 + // Reject anything that isn't a plain https:// hostname resolving to a public,
52 + // non-reserved IP — blocks loopback, RFC1918, link-local/cloud-metadata
53 + // (169.254.0.0/16), and raw IP-literal hosts (real vendor tenants are never IPs).
54 + public static function isSafeRemoteUrl($url)
55 + {
56 + if (!is_string($url) || $url === '') {
57 + return false;
58 + }
59 +
60 + $parts = wp_parse_url($url);
61 + if (empty($parts['scheme']) || strtolower($parts['scheme']) !== 'https' || empty($parts['host'])) {
62 + return false;
63 + }
64 +
65 + if (!empty($parts['user']) || !empty($parts['pass'])) {
66 + return false;
67 + }
68 +
69 + $host = trim($parts['host'], '.');
70 +
71 + if (filter_var($host, FILTER_VALIDATE_IP)) {
72 + return false;
73 + }
74 +
75 + $ip = gethostbyname($host);
76 + if ($ip === $host) {
77 + return false;
78 + }
79 +
80 + return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false;
81 + }
82 +
36 83 // Download a file from a remote URL and create a new directory for this if not exists
37 84 // Then save the file to the new directory and move this directory to a new given directory
38 85 public static function downloadFile($remoteUrl, $baseDir, $fileName)
39 86 {
40 - $filePath = $baseDir . $fileName;
87 + if (!self::isSafeRemoteUrl($remoteUrl)) {
88 + return new \WP_Error('import_unsafe_url', __('Attachment URL is not allowed.', 'fluent-support'));
89 + }
41 90
42 - if (!file_exists($baseDir)) {
43 - mkdir($baseDir, 0777, true);
91 + $baseDir = trailingslashit($baseDir);
92 +
93 + if (!wp_mkdir_p($baseDir)) {
94 + return new \WP_Error('import_dir_error', __('Could not create the attachment import directory.', 'fluent-support'));
44 95 }
45 96
46 - if (!file_exists($filePath)) {
47 - $response = wp_remote_get($remoteUrl, [
48 - 'timeout' => 60,
49 - 'stream' => false
50 - ]);
97 + $realBaseDir = realpath($baseDir);
98 + if ($realBaseDir === false) {
99 + return new \WP_Error('import_dir_error', __('Could not resolve the attachment import directory.', 'fluent-support'));
100 + }
101 + $realBaseDir = rtrim($realBaseDir, '/\\') . DIRECTORY_SEPARATOR;
51 102
52 - if (is_wp_error($response)) {
53 - return false;
54 - }
103 + $safeFileName = self::sanitizeAttachmentFilename($fileName);
104 + $existingPath = $baseDir . $safeFileName;
55 105
56 - $file_contents = wp_remote_retrieve_body($response);
57 - if (empty($file_contents)) {
58 - return false;
59 - }
106 + // Preserve the original dedupe behaviour: if this exact attachment was
107 + // already imported for this ticket, reuse it instead of re-downloading
108 + // (wp_unique_filename() would otherwise mint a "-1" copy every re-import).
109 + if (file_exists($existingPath)) {
110 + $filePath = $existingPath;
111 + $uniqueFileName = $safeFileName;
112 + } else {
113 + $uniqueFileName = wp_unique_filename($baseDir, $safeFileName);
114 + $filePath = $baseDir . $uniqueFileName;
115 + }
60 116
61 - file_put_contents($filePath, $file_contents);
117 + // Belt-and-braces: the resolved parent directory must still be the base dir.
118 + $resolvedParent = realpath(dirname($filePath));
119 + if ($resolvedParent === false || strpos($resolvedParent . DIRECTORY_SEPARATOR, $realBaseDir) !== 0) {
120 + return new \WP_Error('import_path_error', __('Resolved attachment path escapes the import directory.', 'fluent-support'));
121 + }
122 +
123 + if (file_exists($filePath)) {
124 + return $filePath;
125 + }
126 +
127 + $maxBytes = 25 * MB_IN_BYTES;
128 +
129 + // Cap the transfer itself (not just the assembled body) so an oversized
130 + // remote response can't be fully pulled into memory before we reject it.
131 + // Ask for one byte over the limit so a truncated response (real size > cap)
132 + // can still be told apart from a response that legitimately ends at the cap.
133 + $response = wp_safe_remote_get($remoteUrl, [
134 + 'timeout' => 60,
135 + 'stream' => false,
136 + 'limit_response_size' => $maxBytes + 1,
137 + 'redirection' => 0,
138 + ]);
139 +
140 + if (is_wp_error($response)) {
141 + return $response;
142 + }
143 +
144 + $status = (int) wp_remote_retrieve_response_code($response);
145 + if ($status < 200 || $status >= 300) {
146 + return new \WP_Error('import_download_error', sprintf(__('Attachment download failed with HTTP status %d.', 'fluent-support'), $status));
147 + }
148 +
149 + $file_contents = wp_remote_retrieve_body($response);
150 + if (empty($file_contents)) {
151 + return new \WP_Error('import_download_error', __('Attachment download returned an empty response.', 'fluent-support'));
152 + }
153 +
154 + if (strlen($file_contents) > $maxBytes) {
155 + return new \WP_Error('import_download_error', __('Attachment exceeds the maximum allowed download size.', 'fluent-support'));
156 + }
157 +
158 + $tmpPath = $baseDir . '.' . $uniqueFileName . '-' . uniqid('', true) . '.part';
159 + if (file_put_contents($tmpPath, $file_contents) === false) {
160 + return new \WP_Error('import_write_error', __('Could not write the downloaded attachment.', 'fluent-support'));
161 + }
162 +
163 + $fileType = wp_check_filetype_and_ext($tmpPath, $uniqueFileName);
164 + if (empty($fileType['ext']) || empty($fileType['type'])) {
165 + @unlink($tmpPath);
166 + return new \WP_Error('import_filetype_error', __('Attachment file type is not allowed.', 'fluent-support'));
167 + }
168 +
169 + if (!@rename($tmpPath, $filePath)) {
170 + @unlink($tmpPath);
171 + return new \WP_Error('import_write_error', __('Could not save the downloaded attachment.', 'fluent-support'));
62 172 }
63 173
64 174 return $filePath;
65 175 }