PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Core/PluginInstaller.php +55 -18 4.5.64.9.2 View file →
@@ -53,31 +53,68 @@
53 53 if (empty($slug)) {
54 54 return new WP_Error('empty_arg', __('Argument should not be empty.', 'betterdocs'));
55 55 }
56 56
57 - $response = wp_remote_post(
58 - 'http://api.wordpress.org/plugins/info/1.0/',
59 - [
60 - 'body' => [
61 - 'action' => 'plugin_information',
62 - 'request' => serialize((object) [
63 - 'slug' => $slug,
64 - 'fields' => [
65 - 'version' => false,
66 - ],
67 - ]),
68 - ],
69 - ]
70 - );
57 + // Use core's plugins_api() instead of a hand-rolled request. The old code
58 + // POSTed to plaintext http://api.wordpress.org and passed the response
59 + // body straight to unserialize(), so anyone able to intercept that
60 + // connection could inject a PHP-object-injection payload or a malicious
61 + // download_link. plugins_api() talks to api.wordpress.org over HTTPS and
62 + // returns a decoded object — no plaintext transport, no unserialize().
63 + if (!function_exists('plugins_api')) {
64 + include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
65 + }
71 66
72 - if (is_wp_error($response)) {
73 - return $response;
67 + $response = plugins_api('plugin_information', [
68 + 'slug' => $slug,
69 + 'fields' => [
70 + 'version' => true,
71 + ],
72 + ]);
73 +
74 + if (is_wp_error($response) || !is_object($response)) {
75 + return is_wp_error($response) ? $response : new WP_Error('plugins_api_failed', __('Could not retrieve plugin information.', 'betterdocs'));
74 76 }
75 77
76 - return unserialize(wp_remote_retrieve_body($response));
78 + // Bind the package to the requested slug and to an https WordPress.org
79 + // host before anything installs it.
80 + if (isset($response->slug) && $response->slug !== $slug) {
81 + return new WP_Error('slug_mismatch', __('Plugin information did not match the requested plugin.', 'betterdocs'));
82 + }
83 +
84 + if (isset($response->download_link) && !$this->is_allowed_package_url($response->download_link)) {
85 + return new WP_Error('bad_package_host', __('Plugin download URL is not an approved WordPress.org address.', 'betterdocs'));
86 + }
87 +
88 + return $response;
77 89 }
78 90
79 91 /**
92 + * Whether a package URL is safe to hand to the upgrader: https on a
93 + * WordPress.org host. Prevents a tampered response from redirecting the
94 + * install to an attacker-controlled archive.
95 + *
96 + * @param string $url
97 + * @return bool
98 + */
99 + protected function is_allowed_package_url($url)
100 + {
101 + if (!is_string($url) || $url === '') {
102 + return false;
103 + }
104 +
105 + $parts = wp_parse_url($url);
106 +
107 + if (empty($parts['scheme']) || strtolower($parts['scheme']) !== 'https' || empty($parts['host'])) {
108 + return false;
109 + }
110 +
111 + $host = strtolower($parts['host']);
112 +
113 + return in_array($host, ['downloads.wordpress.org', 'wordpress.org', 'www.wordpress.org'], true);
114 + }
115 +
116 + /**
80 117 * install_plugin
81 118 *
82 119 * @param mixed $slug
83 120 * @param bool $active
@@ -129,9 +166,9 @@
129 166 * @return mixed bool|WP_Error
130 167 */
131 168 public function upgrade_plugin($basename = '')
132 169 {
133 - if (empty($slug)) {
170 + if (empty($basename)) {
134 171 return new WP_Error('empty_arg', __('Argument should not be empty.', 'betterdocs'));
135 172 }
136 173
137 174 include_once ABSPATH . 'wp-admin/includes/file.php';