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_error and 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_contacts
- save_deal, delete_deal, update_deal_field, get_pipeline
- save_task, delete_task, complete_task
- save_automation, delete_automation, toggle_automation
- test_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, such as wpestate_crm_sanitize_contact_data, 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(). There is 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 include 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
There is no explicit in-plugin rate limiting. The plugin relies on WordPress and any server or WAF layer. For exposed endpoints such as test SMS, test HubSpot, and 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_settings option, which is the standard WordPress pattern. For higher-security deployments, move them to wp-config.php constants and read them 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, meaning who changed what and when. It is not a full audit log because 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() or current_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.