Security hardening: authorization, input validation, and logging#2923
Conversation
- authorize and scope cluster_geojson, get_grid_totals, get_list_by_grid_id and points_geojson callbacks - personal map limits results to records shared with the current user; records map requires all-access or project metrics permission
cast the user id to an integer before building the meta_value clause so a crafted value.ID can no longer break out of the query
- validate the transfer token before inserting or processing meta, rejecting forged or missing tokens - decode postmeta values with allowed_classes disabled so serialized objects can no longer be instantiated
wp_get_current_user() always returns an object, so check ->exists() so the settings endpoint fails closed for logged-out requests
remove leftover debug error_log calls in the dt-home endpoint that wrote the magic-link public_key to the log on every request
remove the dt_write_log call that wrote the new password to debug.log
render activity revert values, merge target name, and delete-filter name as text instead of html to prevent stored xss
both operations write to records but were gated on can_view; check can_update so a view-only user cannot revert or merge records
cap repeated failed logins per client ip on /jwt-auth/v1/token using transients; scoped to the token endpoint and disableable via filter
the prior guard was unreachable; a user with only a share could remove the assigned user's share and lock them out. allow self-removal, and restrict removing the assignee's share to that user or update_any holders
detect the real mime type from the file bytes instead of trusting the client, and store anything that is not a raster image as octet-stream
replace loose == checks with hash_equals so token matching is constant-time and not subject to numeric string type juggling
reject non-http(s) schemes and hosts resolving to private, loopback, link-local or reserved ranges, and fetch via the http api
the locale clause was appended after the search parenthesis, so its OR escaped the share gate; move it inside so access filters still apply
state the reason (the user is assigned to the record) and make the message translatable instead of a generic permission error
Code ReviewMedium
The PR adds // line 195-200 — capability is always contacts, even if $params['post_type'] is 'groups'
if ( ( $slug === 'personal' ) && current_user_can( 'access_contacts' ) ) {
$has_permission = true;
}
if ( ( $slug === 'records' ) && ( current_user_can( 'dt_all_access_contacts' ) || current_user_can( 'view_project_metrics' ) ) ) {
$has_permission = true;
}A user who holds
set_transient( $key, (int) get_transient( $key ) + 1, $window );
SummaryThe PR is a meaningful security hardening set — cleaning up debug |
Summary
Security hardening across authorization, input validation, authentication, and logging. These came out of a focused security review of the theme. None of these are exploitable by an unauthenticated attacker — the global REST authentication wall (
restrict-rest-api.php) blocks anonymous REST access, so every item below requires at least a low-privilege authenticated session; several require a specific capability. The changes are additive guards and validation with no intended behavior change for legitimate users.Changes by area
Access control / authorization
dynamic_records_mapmetrics endpoints (cluster_geojson/points_geojson/get_grid_totals/get_list_by_grid_id) now apply the same capability +dt_sharescoping aspost_type_geojson, instead of returning every located record's name + coordinates to any authenticated user.merge_postsandrevert_post_activity_historynow requirecan_update(both write to records but were gated oncan_view).remove_sharednow protects the assigned user's foundational share (the previous guard was unreachable) and explicitly allows self-removal.ORclause no longer escapes the share/status/type filters.Injection
date_range_activity: cast theuser_selectvalue to an integer before building the meta-value clause (was concatenated into the query unescaped).receive-transfer: validate the transfer token before processing, and decode incoming postmeta withunserialize(..., ['allowed_classes' => false])to remove a PHP object-injection sink.Input / upload validation
finfo) instead of trusting the client, and store anything that isn't a raster image asapplication/octet-streamso uploads can't be served as inline-executable content.plugin-installnow rejects non-http(s)schemes and hosts that resolve to private/loopback/link-local/reserved ranges, and downloads via the WP HTTP API (closes a server-side request forgery vector; admin-only endpoint).Authentication hardening
jwt-auth/v1/tokenendpoint (transient-based, scoped to that endpoint, disable-able via thedt_enable_jwt_login_throttlefilter for sites that already run a security plugin).get_settingsuses a real!$user->exists()check instead of an always-false guard.hash_equals()(constant-time; removes a numeric-string type-juggling edge). Token format is unchanged, so existing site links keep working.Sensitive data in logs
dt_write_log()call that wrote the cleartext new password on password change.error_log()calls in the DT Home magic-link endpoint that logged the request params (including the magic-link key) on every request.Notes
md5→HMAC and validity-window changes are left for a future versioned protocol change.🤖 Generated with Claude Code