BuiltByGo
Powered by WP HealthKitWP HealthKit Companion
v1.3.4 · Audited on 25 August 2026
B
Overall Risk
MEDIUM
0
Critical
0
High
16
Medium
14
Low
Findings (37)
MEDIUMwp-healthkit.php:2009
Uncached remote request: wp_remote_get()
`wp_remote_get()` makes an HTTP request without transient or object caching nearby. If called on every page load, this adds network latency to every request. Found: `$response = wp_remote_get(`
MEDIUMSECURITY.md
No security contact found in the uploaded archive (CRA requirement)
No security email or VDP URL was found in the uploaded archive's plugin header, readme.txt, or SECURITY.md. If those files exist in your repo but weren't included in this ZIP (e.g. a code-only upload), this is an input artifact, not a plugin defect — re-upload the full package. Otherwise: the EU Cyber Resilience Act requires a documented vulnerability reporting contact.
MEDIUMwp-healthkit.php:792
6 transients never purged on uninstall
The plugin sets transients (e.g. `wphk_token_invalid` and 5 more) but the uninstall routine never deletes them. Expired transients linger in wp_options.
MEDIUMwp-healthkit.php:530-560
Health score and issue counts use color as the sole state indicator in the dashboard widget
[1.4.1 Use of Color] The dashboard widget renders the health score in a dynamically chosen color (#16a34a green, #d97706 amber, #dc2626 red) and the critical/high issue counts in hardcoded red and amber inline styles. No icon, label, or text differentiator accompanies the color change to convey the severity tier. Users who cannot distinguish color (approximately 8% of males) receive no alternative cue that the score is in a warning or critical band.
MEDIUMwp-healthkit.php:617-630
Supply-chain alert warning icon (⚠) rendered via HTML entity without visible text alternative for sighted color-blind users
[1.4.1 Use of Color] The supply-chain alert block uses a left orange border (#f59e0b) and the ⚠ entity (aria-hidden='true') to signal danger. The aria-hidden suppresses the symbol from screen readers (correct), but sighted color-blind users who cannot perceive the orange border have no non-color cue that this block is a warning rather than informational content. The word 'Supply chain alert' is present as text, which partially mitigates this, but the visual design relies on the border color as the primary severity indicator.
MEDIUMwp-healthkit.php:742-748
Token-invalid admin notice lacks role='alert' or aria-live for screen reader announcement
[4.1.3 Status Messages] render_token_verify_notice() outputs a 'notice notice-error is-dismissible' div with no ARIA live region role. An error that appears after a background cron job completes (potentially on the next page load) should be announced to screen readers. Without role='alert' or aria-live='assertive', screen reader users may miss the token verification failure.
MEDIUMwp-healthkit.php:393-400
Site Token input missing autocomplete attribute
[1.3.5 Identify Input Purpose] The site token <input type='text'> has autocomplete='off', which is appropriate for a security token. However, WCAG 1.3.5 requires that inputs serving a known purpose expose that purpose via autocomplete. While 'off' is technically valid, the field has no semantic autocomplete token (e.g. 'one-time-code' or a custom value) that would help password managers or assistive technology understand the field's purpose. This is a minor AA concern but worth noting for EAA conformance.
MEDIUMwp-healthkit.php:413-415
Token preview rendered as <code> element without a programmatic label
[1.3.1 Info and Relationships] The connected-state view renders the truncated site token inside a <code id='wphk_token_preview'> element preceded by a <label for='wphk_token_preview'>. A <label> element is only valid when associated with a form control (input, select, textarea, button). Associating it with a <code> element is invalid HTML and will be ignored by assistive technology — the label's for/id pairing has no semantic effect on a non-interactive element.
MEDIUMwp-healthkit.php:1756
api_request() missing User-Agent header — inconsistent with other calls
The api_request() POST helper and api_request_get() GET helper do not set a User-Agent header, while all other direct wp_remote_post/get calls in the plugin explicitly set 'User-Agent: WP-HealthKit-Companion/VERSION'. This inconsistency means inventory syncs and heartbeats are sent with WordPress's default user-agent, which can cause API-side logging/routing issues and makes it harder to debug traffic.
MEDIUMwp-healthkit.php:1108
wphk_audit_jobs option read on every plugin row render (plugins.php)
add_audit_action_link() calls get_option('wphk_audit_jobs', array()) on every plugin row render on plugins.php. WordPress's plugins list calls this filter once per installed plugin, so on a site with 50 plugins this results in 50 get_option() calls. While WordPress object-caches options, the repeated function call overhead and potential cache misses add up.
MEDIUMwp-healthkit.php:1001
fetch_update_warnings() makes one HTTP call per pending plugin update in a loop
fetch_update_warnings() iterates over all pending plugin updates and calls $this->api_request() (a synchronous wp_remote_post) for each one individually. On a site with 20 pending updates this makes 20 sequential HTTP calls in a single cron run. While this runs in a cron context, it can exhaust the cron time limit and delay other cron jobs.
MEDIUMwp-healthkit.php:1620
api_upload_plugin fallback loads entire ZIP into PHP memory as a string
The non-cURL fallback path in api_upload_plugin() calls file_get_contents($zip_path) to load the entire plugin ZIP into a PHP string before building the multipart body. The ZIP size limit is 50MB, meaning this can allocate up to 50MB of additional PHP memory in a single request. The multipart body string then doubles this allocation.
MEDIUMuninstall.php:44
uninstall.php uses get_sites() with number=0 on multisite — unbounded query
get_sites(array('number' => 0)) fetches ALL sites in a multisite network with no limit. On large networks with thousands of sites this returns a massive result set, consuming significant memory and DB query time during uninstall.
MEDIUMwp-healthkit.php:296
admin.css enqueued twice — once for plugins.php and once for settings page
enqueue_admin_assets() enqueues 'wphk-admin' CSS with the same handle in two separate code paths (plugins.php branch and settings_page branch). While WordPress deduplicates by handle, the code structure means the same style registration logic runs twice, and if the hook order ever changes both could fire on the same page.
MEDIUMwp-healthkit.php:893
site_transient_update_plugins filter fires on every transient read without early exit for disconnected sites
add_safety_warnings_to_updates() is hooked to site_transient_update_plugins which fires multiple times per admin page load. The credentials check (token + site_id) is done inside the filter but only after checking transient->response is non-empty. The duplicate empty check for transient->response (lines 897 and 907) adds unnecessary overhead.
MEDIUMuninstall.php:52-61
Cron events cleared in uninstall.php without multisite site-loop — only clears network-level events
The `wp_clear_scheduled_hook()` calls at the bottom of `uninstall.php` run once at the network level, outside the `switch_to_blog()` loop. On multisite, WP-Cron events are stored per-site in each site's `wp_options` table (as the `cron` option). Calling `wp_clear_scheduled_hook()` outside the loop only clears events for the current blog. Events scheduled on sub-sites (e.g. `wphk_deferred_sync` triggered by a plugin activation on a sub-site) will remain orphaned.
LOWwp-healthkit.php:604-611
Static per-request memo inside sync_inventory() persists across cron ticks in long-lived CLI/queue workers
sync_inventory() memoizes active_plugins and auto_update_plugins using function-static variables. In a normal PHP-FPM request lifecycle this is fine, but in long-running WP-CLI or persistent worker processes, the static cache will not observe changes made mid-process to those options.
LOWwp-healthkit.php:1275
Self-update package URL uses str_ends_with() host check — belt-and-braces
is_trusted_package_url() correctly restricts to https and to wphealthkit.com or *.wphealthkit.com. This is good. Consider also rejecting URLs containing userinfo (user:pass@) to be defensive, since some parsers can be tricked, though wp_parse_url is robust here.
LOWwp-healthkit.php:1155-1170
ZipArchive iterator follows symlinks by default
zip_and_upload_plugin() uses RecursiveDirectoryIterator with SKIP_DOTS but does not set FOLLOW_SYMLINKS off explicitly, nor does it check for symlinks pointing outside the plugin directory. If a plugin directory contains a symlink to, e.g., /etc or wp-config.php, that file could be zipped and uploaded to WP HealthKit.
LOWwp-healthkit.php:1860
Self-update package lacks cryptographic signature verification
The plugin provides its own update channel via the WP HealthKit API. While the package URL is validated to be HTTPS on a whitelisted host (wphealthkit.com or a subdomain), the downloaded plugin ZIP is not verified against a cryptographic signature (e.g., Ed25519). If the WP HealthKit server is compromised, an attacker could supply a malicious plugin package and the site would install it.
LOWwp-healthkit.php:1742
File operation in request path: file_get_contents()
`file_get_contents()` performs disk I/O that may execute on frontend requests. File operations are slow compared to memory operations and block the PHP process. Found: `$file_contents = file_get_contents( $zip_path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_fil...`
LOWwp-healthkit.php
Plugin header licence not recognised: GPL-2.0-or-later
The plugin header declares `License: GPL-2.0-or-later`, which could not be matched against the GPL compatibility list. Verify it is GPL-2.0-or-later compatible.
LOWwp-healthkit.php:135
Cron scheduled in activation hook without multisite network check
On multisite, plugins can be network-activated (once for all sites) or site-activated. The activation hook callback receives a $network_wide parameter. Without checking it, you may schedule cron jobs incorrectly for multisite network activations. Found in `activate()`: `wp_schedule_event( time(), 'twicedaily', 'wphk_sync_inventory' );`
LOWwp-healthkit.php:440
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `<code id="wphk_token_preview" style="user-select: all;"><?php echo esc_html( substr( $token, 0, 20 ) . '...' ); ?></code...`
LOWwp-healthkit.php:464
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `<span aria-hidden="true" style="color:#d63638;margin-left:2px;">*</span>`
LOWwp-healthkit.php:543
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `<div style="font-size:48px;font-weight:700;color:%1$s" aria-label="%2$s">%3$d</div>`
LOWwp-healthkit.php:587
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `'<p style="margin-top:8px;padding-top:8px;border-top:1px solid #eee;font-size:12px;color:#666;text-align:center">%s</p>'...`
LOWwp-healthkit.php:643
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `'<div style="margin-top:8px;padding:10px;background:#fff7ed;border-left:4px solid #f59e0b;border-radius:3px;font-size:12...`
LOWwp-healthkit.php:1482
Inline `style=""` attribute in admin output
An inline `style=""` attribute is used in admin HTML output. Found: `'<span class="wphk-grade-badge" style="--wphk-grade:%s" aria-label="%s">%s</span>',`
LOWwp-healthkit.php:1565
String concatenation used to build multipart body in a loop
api_upload_plugin() builds the multipart body string using .= concatenation inside a foreach loop over $fields. For small field counts this is negligible, but it's a pattern that should use array + implode for consistency with PHP best practices.
INFOwp-healthkit.php:129
load_plugin_textdomain() is unnecessary on WP 4.6+
Since WordPress 4.6, translations are loaded automatically for plugins hosted on wp.org. For self-hosted plugins served through WP HealthKit, the call is still valid but hooking it on 'init' is fine. This is not a bug, just a minor modernization note.
INFOwp-healthkit.php:696
Indentation bug on grades_memo assignment
The line '$this->grades_memo = $grades;' is indented one tab out from its surrounding block inside sync_inventory(). Functionally correct (still inside the if block) but visually misleading — a reader could think it runs unconditionally.
INFOwp-healthkit.php:1
WordPress 7 compatibility check (latest: 7.0)
WordPress 7.0 shipped on 2026-05-20 and is now the current stable. The audit pipeline pulls WP 7 release notes from the `wp/v7-release` doc-cache entry (sourced from Context7) so AI suggestions reflect the new deprecations rather than stale training data.
INFOwp-healthkit.php:407-420
Disconnect and Sync Now buttons are inside a <form> but are type='button' — correct pattern confirmed
[2.4.3 Focus Order] The 'Sync Now' and 'Disconnect Site' buttons are correctly typed as type='button' to prevent accidental form submission. They are native <button> elements and are therefore keyboard accessible by default. This is an informational observation confirming correct implementation.
INFOwp-healthkit.php:330
aria-live polite announcer region present on settings page
[1.3.1 Info and Relationships] The settings page correctly includes <div id='wphk-status-announcer' aria-live='polite' aria-atomic='true' class='screen-reader-text'></div> for dynamic status announcements. This is a positive accessibility pattern. The JS in assets/admin.js (not provided) should populate this element on AJAX responses.
INFOwp-healthkit.php:820
wphk_remote_config stored in wp_options — consider size monitoring
update_option('wphk_remote_config', $response['config'], false) stores the remote config object from the heartbeat API response. The size of this object is controlled by the external API. If the API ever returns a large config object, this option row could grow significantly. The option is non-autoloaded (false), which is correct.
INFOwp-healthkit.php:1680
check_for_updates() uses wp_remote_get without wp_safe_remote_get
The self-update check uses wp_remote_get() directly. WordPress best practices recommend wp_safe_remote_get() for dynamic URLs to prevent SSRF. The URL is hardcoded here so the risk is minimal, but the pattern is worth noting.
Audited by BuiltByGo via WP HealthKit