-
Notifications
You must be signed in to change notification settings - Fork 9
Description
Full audit of the current main branch (ddad8c0). Issues grouped by severity.
Critical
1. SSRF — unsanitized URL accepted from user input and passed directly to cURL
In webseer.php form_save():
$save['url'] = get_nfilter_request_var('url');get_nfilter_request_var() applies no validation of scheme, host, or path. This value is stored directly to plugin_webseer_urls.url, then webseer_process.php makes an unconditional cURL request to it:
$results = $cc->get($url['url']);Any authenticated user with access to the webseer configuration page can create a service check pointing to:
file:///etc/passwd— local file readhttp://169.254.169.254/latest/meta-data/— cloud instance metadata (AWS/GCP/Azure credential theft)http://127.0.0.1:6379/— internal Redis/Memcached/Elasticsearch probingdict://,gopher://,sftp://— protocol-level attacks depending on libcurl build
Fix: validate that url matches ^https?:// before saving. For strict defense also apply an allowlist of permitted hosts/CIDRs and block RFC-1918 ranges via a pre-flight IP resolution check.
2. PHP object injection — unserialize() on HTTP response data in includes/functions.php
$servers = unserialize(base64_decode($servers));
$urls = unserialize(base64_decode($urls));Both plugin_webseer_refresh_servers() and plugin_webseer_refresh_urls() fetch data from a remote server via cURL, then call unserialize() on the response body. If any remote webseer server is compromised, or if the connection is subject to a MITM attack (no cert pinning, no HMAC), the attacker controls the deserialized object graph — leading to PHP object injection and likely RCE via available gadget chains in the autoloader (e.g. Cacti core, Monolog, any Composer dependency).
Fix: replace serialize/unserialize with json_encode/json_decode for inter-server transport. Add a shared-secret HMAC header for response integrity if the serialized format cannot be changed immediately.
3. SQL injection — email_address directly interpolated in plugin_webseer_update_contacts()
db_execute("REPLACE INTO plugin_webseer_contacts (id, user_id, type, data) VALUES ($cid, " . $u['id'] . ", 'email', '" . $u['email_address'] . "')");$u['email_address'] is read from user_auth but was originally entered by a user. An email address containing a single quote (e.g. o'brien@example.com) breaks the query syntax. A crafted value like x@x.com', 0); DROP TABLE plugin_webseer_contacts; -- achieves arbitrary SQL execution. Use db_execute_prepared():
db_execute_prepared(
'REPLACE INTO plugin_webseer_contacts (id, user_id, type, data) VALUES (?, ?, ?, ?)',
[$cid, $u['id'], 'email', $u['email_address']]
);The same function has a second identical query for the insert-without-id path with the same vulnerability.
4. SQL injection — rfilter directly interpolated into RLIKE clause in list_urls()
$sql_where .= '... display_name RLIKE \'' . get_request_var('rfilter') . '\' OR ' .
'url RLIKE \'' . get_request_var('rfilter') . '\' ...';rfilter is validated with FILTER_VALIDATE_IS_REGEX, which only confirms the value is a valid regular expression — it does not prevent SQL metacharacters. A value like a' OR '1'='1 is a valid regex (matches a) and injects into the SQL. MySQL RLIKE clauses accept a string parameter and should use a prepared statement with ? placeholders:
$sql_where .= '(display_name RLIKE ? OR url RLIKE ? OR search RLIKE ? OR search_maint RLIKE ? OR search_failed RLIKE ?)';
$sql_params = array_merge($sql_params, array_fill(0, 5, get_request_var('rfilter')));High
5. notify_accounts IN clause uses raw string interpolation
$users = db_fetch_cell("SELECT GROUP_CONCAT(DISTINCT data) AS emails
FROM plugin_webseer_contacts
WHERE id IN (" . $url['notify_accounts'] . ")");notify_accounts is stored as a comma-separated integer string from form_save(), where individual elements are validated with input_validate_input_number(). This is a time-of-check/time-of-use pattern: the value is safe when saved through the normal UI path, but the SQL construction is inherently fragile and bypasses the prepared statement layer entirely. Any future code path that writes to notify_accounts without the per-element validation (e.g. the inter-server sync functions) bypasses the guard. Use db_build_in_clause() or convert to JSON storage.
6. No URL scheme validation allows file://, gopher://, and other dangerous cURL protocols
Even if SSRF is addressed for RFC-1918 ranges (#1), cURL supports file://, dict://, sftp://, ldap://, gopher:// and others that are unrelated to HTTP service checking. The url field should be validated to allow only http:// and https:// schemes before the record is saved. Additionally, curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS) should be set in the cURL class to enforce this at the transport layer regardless of what is stored.
Medium
7. SELECT * on plugin_webseer_urls fetches proxy credentials into memory
In webseer_process.php:
$url = db_fetch_row_prepared('SELECT * FROM plugin_webseer_urls WHERE ...');This includes notify_accounts, search_failed, and any other columns. More critically, proxy records are fetched with SELECT * as well:
$proxy = db_fetch_row_prepared('SELECT * FROM plugin_webseer_proxies WHERE id = ?', ...);Proxy records contain username and password. Fetch only the columns actually used by the poller.
8. ca-bundle.crt committed to the repository
A 225 KB CA bundle is committed as ca-bundle.crt and is presumably passed to cURL for SSL verification. Bundled CA stores go stale and are not updated by OS/distro security patches. Use the system CA store via curl_setopt($ch, CURLOPT_CAINFO, null) and let libcurl use its compiled-in default, or document how to configure the path via settings rather than hardcoding a stale bundle in the repo.
Low
9. plugin_webseer_set_remote_master() passes $url (array) where string expected
function plugin_webseer_set_remote_master($url, $ip) {
$cc = new cURL(true, 'cookies.txt', 'gzip', '', $url);
...
$results = $cc->post($url['url'], $data); // $url is array, used as string in constructor
}The function is called with $server['url'] (a string) from plugin_webseer_set_remote_masters(), but the cURL constructor receives $url which at that point is the string value, while $url['url'] on the post() call would be null (string index access). This is a latent bug — the function is probably never called in the primary code path given the ambiguous parameter.
10. plugin_webseer_amimaster() and plugin_webseer_whoami() use gethostbyname() for identity
Server identity is determined by resolving the local hostname to an IP and comparing against the DB. This is spoofable if DNS is attacker-controlled and means the master-election logic has an external dependency on DNS integrity. Use a stored per-server UUID or a locally-configured server ID instead.