PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 2.11.3
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v2.11.3
3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 2.9.9 2.9.8 2.9.6 2.9.7 2.9.5 All 88 releases
vigilante / includes / security-analyzer / class-sa-category-reputation.php

class-sa-category-reputation.php in Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… 2.11.3, at includes/security-analyzer/class-sa-category-reputation.php

278 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Security Analyzer — Reputation / blacklist category (informational, 0 pts).
4 *
5 * DNS-based reputation lookups against public blacklists. Zero HTTP APIs, no
6 * account required: everything is a cheap DNS A-record query. Because a listing
7 * can be transient (a shared IP or mass scanner's fault), these checks are
8 * informational — they surface listings without deducting from the overall score.
9 *
10 * Queried blacklists:
11 * - Spamhaus ZEN (zen.spamhaus.org) — aggregated SBL+XBL+PBL.
12 * - Barracuda BRBL (b.barracudacentral.org).
13 * - SpamCop SCBL (bl.spamcop.net).
14 *
15 * The server IP is resolved from the site URL's host (same physical host in
16 * most self-hosted WP installs). Reverse the octets and prepend them to each
17 * blacklist zone; a successful A lookup means the IP is listed.
18 *
19 * @package Vigilante
20 * @since 2.1.0
21 */
22
23 // Prevent direct access.
24 if ( ! defined( 'ABSPATH' ) ) {
25 exit;
26 }
27
28 /**
29 * DNS-based reputation checks (all informational).
30 */
31 class Vigilante_SA_Category_Reputation {
32
33 const SLUG = 'reputation';
34
35 /**
36 * Blacklists we query. DNS-only, no authentication.
37 *
38 * @var array<string,array{label:string,zone:string,info_url:string}>
39 */
40 private static $blacklists = array(
41 'spamhaus' => array(
42 'label' => 'Spamhaus ZEN',
43 'zone' => 'zen.spamhaus.org',
44 'info_url' => 'https://check.spamhaus.org/',
45 ),
46 'barracuda' => array(
47 'label' => 'Barracuda BRBL',
48 'zone' => 'b.barracudacentral.org',
49 'info_url' => 'https://www.barracudacentral.org/rbl/removal-request',
50 ),
51 'spamcop' => array(
52 'label' => 'SpamCop SCBL',
53 'zone' => 'bl.spamcop.net',
54 'info_url' => 'https://www.spamcop.net/bl.shtml',
55 ),
56 );
57
58 /**
59 * @var Vigilante_Settings
60 */
61 private $settings;
62
63 public function __construct( Vigilante_Settings $settings ) {
64 $this->settings = $settings;
65 }
66
67 /**
68 * Run the category.
69 *
70 * All reputation checks require DNS + external network, so they live in the
71 * 'slow' phase.
72 *
73 * @param string $phase 'fast' | 'slow' | 'all'.
74 * @return Vigilante_SA_Check_Result[]
75 */
76 public function run( $phase = 'all' ) {
77 if ( 'fast' === $phase ) {
78 return array();
79 }
80
81 $results = array();
82
83 $ip = $this->resolve_site_ip();
84
85 // A site on a private or reserved address (a local install, a staging box
86 // on an intranet, anything behind NAT resolving to its LAN address) has no
87 // public reputation to query. Sending those to a DNSBL leaks the internal
88 // address to a third party and can only ever answer nonsense.
89 $is_public = '' !== $ip && filter_var(
90 $ip,
91 FILTER_VALIDATE_IP,
92 FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
93 );
94
95 // Intro/diagnostic row telling the user what was tested and on what IP.
96 $results[] = $this->build_intro( $ip, $is_public );
97
98 if ( '' === $ip || ! $is_public ) {
99 // Nothing public to query — the intro row already explains why.
100 return $results;
101 }
102
103 foreach ( self::$blacklists as $key => $meta ) {
104 $results[] = $this->check_blacklist( $key, $meta, $ip );
105 }
106
107 return $results;
108 }
109
110 /**
111 * Introductory info row that documents the IP we tested.
112 *
113 * @param string $ip
114 * @return Vigilante_SA_Check_Result
115 */
116 private function build_intro( $ip, $is_public = true ) {
117 $args = array(
118 'id' => 'reputation_overview',
119 'category' => self::SLUG,
120 'max' => 0,
121 'label' => __( 'Site IP for blacklist queries', 'vigilante' ),
122 'fix_link' => '',
123 );
124
125 if ( '' === $ip ) {
126 $args['detail'] = __( 'Could not resolve the site\'s public IP (DNS lookup failed). Reputation checks skipped — this is typically a transient resolver issue.', 'vigilante' );
127 return Vigilante_SA_Check_Result::skip( $args );
128 }
129
130 if ( ! $is_public ) {
131 $args['detail'] = sprintf(
132 /* translators: 1: site host, 2: resolved IPv4 */
133 __( '%1$s resolves to %2$s, a private or reserved address. Public blacklists only track addresses reachable from the internet, so these checks were skipped.', 'vigilante' ),
134 wp_parse_url( home_url(), PHP_URL_HOST ),
135 $ip
136 );
137 $args['data'] = array( 'ip' => $ip );
138 return Vigilante_SA_Check_Result::skip( $args );
139 }
140
141 $args['detail'] = sprintf(
142 /* translators: 1: site host, 2: resolved IPv4 */
143 __( '%1$s resolves to %2$s. We query public DNS blacklists (DNSBLs) against this address. These checks are informational — a listing does not deduct from your score.', 'vigilante' ),
144 wp_parse_url( home_url(), PHP_URL_HOST ),
145 $ip
146 );
147 $args['data'] = array( 'ip' => $ip );
148 return Vigilante_SA_Check_Result::info( $args );
149 }
150
151 /**
152 * Query a single DNSBL for the given IP.
153 *
154 * @param string $key Slug (spamhaus|barracuda|spamcop).
155 * @param array $meta Metadata (label, zone, info_url).
156 * @param string $ip IPv4 address.
157 * @return Vigilante_SA_Check_Result
158 */
159 private function check_blacklist( $key, array $meta, $ip ) {
160 $args = array(
161 'id' => 'reputation_' . $key,
162 'category' => self::SLUG,
163 'max' => 0,
164 'label' => sprintf(
165 /* translators: %s: blacklist name */
166 __( '%s blacklist', 'vigilante' ),
167 $meta['label']
168 ),
169 'fix_link' => $meta['info_url'],
170 );
171
172 $reversed = $this->reverse_ip( $ip );
173 if ( '' === $reversed ) {
174 $args['detail'] = __( 'Invalid IPv4 address, skipping.', 'vigilante' );
175 return Vigilante_SA_Check_Result::skip( $args );
176 }
177
178 $hostname = $reversed . '.' . $meta['zone'];
179
180 // Use gethostbynamel() to avoid long single-host timeouts and get all A records back.
181 // A listing answers with a 127.0.0.x code; a clean address answers nothing.
182 $records = @gethostbynamel( $hostname ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
183
184 if ( false === $records || empty( $records ) ) {
185 $args['detail'] = sprintf(
186 /* translators: %s: blacklist name */
187 __( 'Not listed on %s.', 'vigilante' ),
188 $meta['label']
189 );
190 return Vigilante_SA_Check_Result::info( $args );
191 }
192
193 $codes = array_values( array_filter( (array) $records, 'is_string' ) );
194
195 /*
196 * An answer is not the same as a listing. The DNSBLs reserve 127.255.255.x
197 * to say "I am not answering you": Spamhaus returns 127.255.255.252 for a
198 * typo in the query, .254 for a query coming from an open or public
199 * resolver (Google DNS, Cloudflare and the like, which is what most shared
200 * hosting uses) and .255 for too many queries. Reading those as a listing
201 * told sites they were on Spamhaus when they were not, and there was no way
202 * for the owner to act on it because there was nothing to delist.
203 */
204 $refused = array();
205 $listed = array();
206
207 foreach ( $codes as $code ) {
208 if ( 0 === strpos( $code, '127.255.255.' ) ) {
209 $refused[] = $code;
210 } else {
211 $listed[] = $code;
212 }
213 }
214
215 if ( empty( $listed ) ) {
216 $args['detail'] = sprintf(
217 /* translators: 1: blacklist name, 2: response codes (127.255.255.x) */
218 __( '%1$s did not answer the query (response: %2$s). That code means the blacklist refused the lookup, usually because the server asks through a public DNS resolver, not that the address is listed. Nothing to fix on the site.', 'vigilante' ),
219 $meta['label'],
220 implode( ', ', $refused )
221 );
222 $args['data'] = array( 'refused' => $refused );
223 return Vigilante_SA_Check_Result::skip( $args );
224 }
225
226 // Genuinely listed. Informational in terms of scoring (max=0), but flagged
227 // as a warning so it stands out.
228 $args['detail'] = sprintf(
229 /* translators: 1: blacklist name, 2: response codes (127.0.0.x) */
230 __( 'Listed on %1$s (response: %2$s). Shared hosting? Check if the listing belongs to your IP range and request delisting from the blacklist operator.', 'vigilante' ),
231 $meta['label'],
232 implode( ', ', $listed )
233 );
234 $args['data'] = array( 'response' => $listed );
235 return Vigilante_SA_Check_Result::warn( $args );
236 }
237
238 /**
239 * Resolve the site's public IPv4 address.
240 *
241 * @return string IPv4 or '' on failure.
242 */
243 private function resolve_site_ip() {
244 $host = wp_parse_url( home_url(), PHP_URL_HOST );
245 if ( ! $host ) {
246 return '';
247 }
248
249 // Already an IP? Short-circuit.
250 if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
251 return $host;
252 }
253
254 $ip = @gethostbyname( $host ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
255 if ( $ip === $host ) {
256 return ''; // gethostbyname returns the input on failure.
257 }
258 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
259 return ''; // DNSBLs we use are IPv4-only.
260 }
261 return $ip;
262 }
263
264 /**
265 * Reverse an IPv4 address (1.2.3.4 → 4.3.2.1). Return '' if invalid.
266 *
267 * @param string $ip
268 * @return string
269 */
270 private function reverse_ip( $ip ) {
271 if ( ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
272 return '';
273 }
274 $parts = array_reverse( explode( '.', $ip ) );
275 return implode( '.', $parts );
276 }
277 }
278