| @@ -32,15 +32,15 @@ | ||
| 32 | 32 | |
| 33 | 33 | $inputs = FormFieldsParser::getEntryInputs($form); |
| 34 | 34 | |
| 35 | 35 | $labels = FormFieldsParser::getAdminLabels($form, $inputs); |
| 36 | - | |
| 36 | + | |
| 37 | 37 | $labels = apply_filters_deprecated( |
| 38 | 38 | 'fluentform_slack_field_label_selection', |
| 39 | 39 | [ |
| 40 | 40 | $labels, |
| 41 | 41 | $settings, |
| 42 | - $form | |
| 42 | + $form, | |
| 43 | 43 | ], |
| 44 | 44 | FLUENTFORM_FRAMEWORK_UPGRADE, |
| 45 | 45 | 'fluentform/slack_field_label_selection', |
| 46 | 46 | 'Use fluentform/slack_field_label_selection instead of fluentform_slack_field_label_selection.' |
| @@ -67,9 +67,9 @@ | ||
| 67 | 67 | } |
| 68 | 68 | |
| 69 | 69 | $footerText = ArrayHelper::get($settings, 'footerText'); |
| 70 | 70 | if ($footerText === '') { |
| 71 | - $footerText = "fluentform"; | |
| 71 | + $footerText = 'fluentform'; | |
| 72 | 72 | } |
| 73 | 73 | |
| 74 | 74 | $fields = []; |
| 75 | 75 | |
| @@ -105,15 +105,48 @@ | ||
| 105 | 105 | 'title' => $title, |
| 106 | 106 | 'title_link' => $titleLink, |
| 107 | 107 | 'fields' => $fields, |
| 108 | 108 | 'footer' => $footerText, |
| 109 | - 'ts' => round(microtime(true) * 1000) | |
| 110 | - ] | |
| 111 | - ] | |
| 112 | - ]) | |
| 109 | + 'ts' => round(microtime(true) * 1000), | |
| 110 | + ], | |
| 111 | + ], | |
| 112 | + ]), | |
| 113 | 113 | ]; |
| 114 | 114 | |
| 115 | - $result = wp_remote_post($slackHook, [ | |
| 115 | + // SECURITY (FINDING-15): the webhook URL is admin-supplied and was fetched with no egress | |
| 116 | + // restriction — an SSRF with a logged status/error oracle (the response is written into the | |
| 117 | + // feed log). wp_safe_remote_post (below) blocks private/loopback ranges; additionally pin | |
| 118 | + // the host, since Slack incoming webhooks are always https://hooks.slack.com/... , so an | |
| 119 | + // arbitrary external host cannot be used as the oracle target either. | |
| 120 | + // COMPAT: filterable so a site using a Slack-compatible endpoint (Mattermost, Rocket.Chat, | |
| 121 | + // an internal relay) can keep an existing feed working without patching. Site PHP only — | |
| 122 | + // a form manager cannot reach it, so the default-deny posture is preserved. | |
| 123 | + $defaultHookHosts = ['hooks.slack.com']; | |
| 124 | + $allowedHookHosts = (array) apply_filters('fluentform/slack_allowed_webhook_hosts', $defaultHookHosts, $feed); | |
| 125 | + $allowedHookHosts = array_map('strtolower', array_filter($allowedHookHosts, 'is_string')); | |
| 126 | + // A filter returning nothing usable must not silently break every Slack feed — fall back | |
| 127 | + // to the official host so the default keeps working no matter what the filter returns. | |
| 128 | + if (!$allowedHookHosts) { | |
| 129 | + $allowedHookHosts = $defaultHookHosts; | |
| 130 | + } | |
| 131 | + | |
| 132 | + $parsedHook = wp_parse_url($slackHook); | |
| 133 | + $hookHost = isset($parsedHook['host']) ? strtolower($parsedHook['host']) : ''; | |
| 134 | + $hookScheme = isset($parsedHook['scheme']) ? strtolower($parsedHook['scheme']) : ''; | |
| 135 | + if ('https' !== $hookScheme || !in_array($hookHost, $allowedHookHosts, true)) { | |
| 136 | + $message = sprintf( | |
| 137 | + // translators: %s is a comma-separated list of allowed webhook hosts. | |
| 138 | + __('Invalid Slack webhook URL. It must be an https:// URL on: %s', 'fluentform'), | |
| 139 | + implode(', ', $allowedHookHosts) | |
| 140 | + ); | |
| 141 | + do_action('fluentform/integration_action_result', $feed, 'failed', $message); | |
| 142 | + return [ | |
| 143 | + 'status' => 'failed', | |
| 144 | + 'message' => $message, | |
| 145 | + ]; | |
| 146 | + } | |
| 147 | + | |
| 148 | + $result = wp_safe_remote_post($slackHook, [ | |
| 116 | 149 | 'method' => 'POST', |
| 117 | 150 | 'timeout' => 30, |
| 118 | 151 | 'redirection' => 5, |
| 119 | 152 | 'httpversion' => '1.0', |