Developer documentation for the security subsystem of the WPResidence real estate CRM. Entry point: libs/security.php.
Nonce Helpers
wpestate_crm_verify_ajax_nonce( $action, $nonce_key = 'security' )
- Reads nonce from
$_POST[$nonce_key]. - Calls
wp_verify_nonce(). - Also requires
is_user_logged_in(). - On failure, sends
wp_send_json_errorand exits.
Nonce Registry
The frontend wpestate_crm_vars object (localized in wpestate-crm.php lines 130-158) exposes 17+ action-specific nonces, plus a generic wpestate_crm_nonce. Every AJAX endpoint has a corresponding nonce action with the same name.
Examples
save_contact,delete_contact,bulk_delete_contacts,bulk_update_contactssave_deal,delete_deal,update_deal_field,get_pipelinesave_task,delete_task,complete_tasksave_automation,delete_automation,toggle_automationtest_hubspot,test_webhook,send_test_sms
Sanitization Helpers
| Function | Behavior |
|---|---|
wpestate_crm_sanitize_text($text) |
strip_tags() + trim() |
wpestate_crm_sanitize_email($email) |
is_email() validation; returns empty string if invalid |
wpestate_crm_sanitize_int($value) |
Returns non-negative integer; 0 for invalid or negative |
Each entity’s CRUD module has its own sanitizer (wpestate_crm_sanitize_contact_data, etc.) that applies per-column rules using these helpers plus sanitize_text_field(), sanitize_textarea_field(), absint(), and esc_url_raw().
Capability Checks
| Check | Purpose |
|---|---|
current_user_can('manage_crm') |
Baseline — every CRM endpoint requires this |
wpestate_crm_user_can($module, $action) |
Module-level role-based permission |
wpestate_crm_user_owns_contact($id) |
Per-record ownership check |
current_user_can('manage_options') |
Settings page and integration test endpoints |
SQL Injection Protection
All database access uses $wpdb->prepare() with placeholder syntax. Examples from the migration code:
$wpdb->get_results( $wpdb->prepare(
"SELECT ID, post_author, post_title, post_content, post_date
FROM {$wpdb->posts}
WHERE post_type = %s AND post_status != 'auto-draft'
ORDER BY ID ASC LIMIT %d OFFSET %d",
$post_type, $limit, $offset
) );
Bulk operations build IN (...) clauses with repeated placeholders and pass the sanitized integer array to prepare(). No string concatenation of user input into SQL.
CSRF Protection
- Every form uses
wp_nonce_field(). - Every AJAX handler verifies the nonce before performing any action.
- GET-based deletes (on list pages) also require a nonce in the URL.
Output Escaping
Templates use esc_html(), esc_attr(), esc_url(), esc_textarea(), and wp_kses_post() appropriately. Activity message bodies that may contain URLs use esc_url() on the URL fragment.
File Upload Hardening
The CSV import handler (libs/csv-functions.php) implements multiple defenses against path traversal — see the CSV developer article for the full list. Key steps: sanitize_file_name() + basename(), server-side path construction from a known base, realpath() validation, MIME check, and is_file() rejection of directories and device nodes.
Rate Limiting
No explicit in-plugin rate limiting. Relies on WordPress and any server/WAF layer. For exposed endpoints (test SMS, test HubSpot, test webhook) the manage_options gate prevents abuse by non-admins.
Sensitive Data Handling
- Twilio Auth Token and HubSpot API token are stored in plain text in the
wpestate_crm_settingsoption — standard WP pattern. For higher-security deployments, move towp-config.phpconstants and read via a filter. - Passwords are never stored by the CRM — the CRM never handles login credentials.
Activity Log vs Audit Log
The activity timeline logs mutations (who changed what and when). It is not a full audit log — it does not log reads. If you need read-audit, attach add_action('wp_ajax_wpestate_crm_*', ...) listeners in a child plugin and write to your own table.
Code Review Checklist for New Endpoints
- Nonce verified with
wpestate_crm_verify_ajax_nonce(). - Capability check via
wpestate_crm_user_can()orcurrent_user_can(). - Per-row ownership check for mutations.
- All input sanitized before use.
- All SQL uses
$wpdb->prepare(). - Output escaped in templates.
- Error responses never leak sensitive information.