ZPortals Plugin Security Overview
This document describes how the ZPortals plugin handles authentication, authorization, data storage, network communication, and updates. It is written for security reviewers, IT administrators, and customers evaluating ZPortals for use with sensitive business data in Zoho.
Contents
- Architecture overview
- Authentication and sessions
- Password storage
- Multi-factor authentication
- Authorization model
- Pre-authentication defenses
- Data storage scope
- OAuth token storage
- Network architecture
- Administrative surface
- SSRF defenses
- SQL injection defenses
- Update delivery
- Activity logging
- Data lifecycle
- Subprocessors
- Privacy & regulatory posture
- Incident response
- Customer responsibilities
- Security review process
- Vulnerability disclosure
Self-hosted, with data flowing directly between you and Zoho
ZPortals is a self-hosted WordPress plugin. All portal logic runs inside your own WordPress installation. CRM record content, file contents, and email message bodies flow directly between that installation and Zoho's APIs over HTTPS, and are not persisted on ZPortals-operated servers.
A small number of supporting flows do transit plugin.zportals.com:
- License validation and refresh (signed HMAC requests).
- Plugin update checks against the ZPortals update server.
- The initial Zoho OAuth handshake, where a ZPortals-operated callback exchanges the authorization code for tokens and then redirects them to your installation. Tokens are held only in process memory for the duration of the redirect and are never written to disk on
plugin.zportals.com.
Each of these flows is documented in more detail in the relevant section below.
The optional Google Maps / Places address-autocomplete feature in the CRM module, when enabled by the administrator, is called directly from the portal user's browser to Google's APIs using the API key the administrator configures in the ZPortals admin panel. That traffic does not transit ZPortals infrastructure; it is listed in the subprocessor table below for transparency about which third parties may receive customer data.
Authentication and session model
Portal users authenticate against WordPress core. When you create or invite a portal user, ZPortals provisions a real WordPress user account via wp_insert_user() and assigns it the client role that the plugin registers on activation.
At login time, ZPortals calls WordPress's wp_signon() and wp_set_auth_cookie() so that portal users hold genuine WordPress auth cookies. The session model, cookie security flags (HttpOnly, Secure, SameSite), and password verification all inherit from the WordPress core implementation that your site is already configured for.
In addition to the WordPress auth cookie, ZPortals maintains a server-side session record so that portal context (selected portal, profile permissions, last activity) persists across page loads. The session is established only after wp_signon() succeeds.
CRM deactivation reconciliation
After the user's password has been verified but before the portal session is established, ZPortals queries the user's linked Zoho CRM Contact record and checks the configured deactivation field. If that field indicates the user has been deactivated in CRM, the login is rejected and the portal user record is marked inactive locally. This means a CRM administrator can revoke portal access from inside Zoho without touching WordPress.
Password storage
Portal passwords are hashed with bcrypt at WordPress's default cost factor. Verification uses WordPress core's standard password-hash functions.
No passwords are stored in plaintext or with reversible encryption at any point.
Multi-factor authentication
ZPortals ships native two-factor authentication. Administrators can configure 2FA as Off, Optional, or Required for the whole portal under WP Admin > ZPortals > Setup > User Registration Settings.
Three verification methods are supported:
- TOTP (RFC 6238), compatible with Google Authenticator, Authy, 1Password, and other standard authenticator apps. TOTP secrets are stored encrypted at rest using AES-256-GCM keyed off the WordPress installation's authentication salt.
- Email OTP for users without smartphones.
- SMS via Twilio for users who prefer text-based codes.
Additional protections:
- Recovery codes are stored hashed with
wp_hash_password()and can be used once each. - Administrators set a maximum attempt count (3-10, default 5); exceeding it locks out the session.
- Trusted-device tokens are stored as SHA-256 hashes and compared with
hash_equals()for constant-time matching. - Trusted devices expire after a configurable interval and can be revoked from the user's security settings page.
Authorization model
Portal users are issued the WordPress client role on creation. That role carries only the read capability so that portal users cannot reach the WordPress administration screens.
On top of WordPress capabilities, ZPortals layers a per-user permission profile system stored in the zportals_users table. Each user is assigned a profile that controls:
- Which Zoho modules they can read and which they can edit (Read / ReadWrite per module).
- Which fields are visible, hidden, or read-only.
- Which custom portal pages are available in their menu.
- Whether they may create, update, or delete records in each module.
Profiles are created and assigned by administrators through the ZPortals admin panel. The profile assignment is enforced on record-level read and write operations. Portal users cannot escalate their own profile or assign themselves a different one.
Administrative actions in the WordPress admin (managing portal users, configuring integrations, editing custom pages, downloading backups) require the WordPress manage_options capability. Every admin-facing AJAX endpoint enforces this capability in code before performing any action.
Pre-authentication defenses
Login rate limiting
ZPortals enforces per-IP and per-username throttling on failed login attempts. The protection is enabled by default and hooks WordPress's authenticate filter ahead of the password check, so locked accounts are rejected before any credential verification runs. Defaults are 5 failed attempts within 15 minutes, both configurable from WP Admin > ZPortals > Setup > User Registration Settings.
Counter state is held in non-persistent storage keyed by salted hashes of the IP address and the lowercase username, so neither the originating IP nor the attempted username is written to the database in plaintext. A successful login clears both counters. Lockouts release automatically when the configured window expires.
Coverage applies to the WordPress admin login, both portal login flows, and any other WordPress authentication entry point.
Bot detection
When an administrator configures Google reCAPTCHA v3 with a site key and secret key, every login attempt is additionally verified against Google's siteverify endpoint and the score is checked against a 0.5 threshold. Failed verification blocks the login attempt before any password check. reCAPTCHA is independent of the rate-limiting protection above; either, both, or neither can be configured.
2FA challenge attempts
After the password step, 2FA code submission has its own attempt limit (default 5, administrator-configurable up to 10). Exceeding it terminates the partial-login session and requires the user to start over.
What is stored in your WordPress database
The ZPortals plugin creates and maintains the following tables in your WordPress database (each is prefixed with your installation's WordPress table prefix):
| Table | Contents |
|---|---|
zportals_users | Portal user accounts: name, email, phone, bcrypt password hash, WordPress user link, Zoho CRM Contact ID, Zoho Books Contact ID, last login, 2FA enrollment state, profile assignment |
zportals_tempusers | Signup data awaiting email verification |
zportals_auth | OAuth access and refresh tokens for each connected Zoho service, encrypted at rest |
zportals_options | Cached Zoho module and field metadata, payment gateway configuration, trusted-device token hashes, license state, plugin settings |
zportals_activity_logs | Optional audit trail of portal user actions and field-value changes (disabled by default) |
zportals_surveys | Survey definitions and submitted responses |
zportals_uploadhistory | File-upload history for portal-side attachments, including the destination Zoho CRM Account ID and Account name so each upload can be filed against the correct Account in CRM and WorkDrive |
zportals_userimport | Temporary state for admin-initiated user imports |
zportals_fieldnames | Administrator-configured display name overrides for Zoho fields |
zportals_field_tooltips | Field-level help text configured by administrators |
zportals_errorlog | Diagnostic error log for support |
CRM record bodies, file contents, and email message bodies are not persisted in any of these tables. They are fetched live from Zoho on each request and rendered into the portal page.
When the plugin is uninstalled through the standard WordPress uninstall flow, every table listed above is dropped via the plugin's uninstall.php. The associated license records on the ZPortals license server are also cleared on uninstall.
Data stored on plugin.zportals.com
The ZPortals server stores B2B vendor-customer information about the administrator who purchased the license, not about your portal end-users:
| Data category | Purpose |
|---|---|
| Customer name, email, company, billing contact | License issuance and subscription billing |
| License key, license type, status | License validation and tier enforcement |
| Website domain(s) registered to the license | Per-domain license enforcement |
| Plugin version and theme version per domain | Update management and changelog targeting |
| Active portal user count per domain (a number only, not a list of users) | Plan-tier compliance |
| API call counts per domain per platform | Usage tracking against monthly/daily limits |
| Zoho Subscriptions customer ID, subscription ID, plan, status | Billing integration with Zoho Subscriptions |
CRM record content, portal end-user names, portal end-user emails or phone numbers, authentication credentials, attachments, and OAuth tokens (after the initial handshake) are not stored on the ZPortals server. Portal end-user data resides exclusively in your own WordPress database, under your control.
This is the standard data set a software vendor maintains for a vendor-customer billing relationship.
OAuth setup and token storage
ZPortals maintains a single OAuth client registration with Zoho, registered in ZPortals's own Zoho account. Each customer authorizes their own Zoho organization to that client during initial portal setup, which produces a customer-specific refresh token bound to that organization.
Initial OAuth flow
Because the Zoho OAuth application has a single configured redirect URI rather than per-customer URIs, the authorization-code exchange is handled by a small endpoint on plugin.zportals.com:
- The administrator clicks "Connect" in their WordPress admin for a given Zoho service. The plugin generates a cryptographically random
statevalue, stores it server-side on your WordPress installation, and includes it in the authorization request to bind the callback to the initiating session. - The browser redirects to
accounts.zoho.comwithredirect_uriset to a ZPortals-operated callback endpoint onplugin.zportals.comand with thestateparameter attached. - The administrator approves the requested scopes.
- Zoho redirects the browser back to
plugin.zportals.comwith an authorization code and thestatevalue. plugin.zportals.comvalidates that the requesting customer's redirect target resolves to a domain registered against the license (the registered domain is established when the license is first activated and updated only through the authenticated customer admin panel; redirect targets outside that domain are rejected), then exchanges the authorization code with Zoho's token endpoint to receive an access token and refresh token.plugin.zportals.comtransmits the tokens to your WordPress installation through a short-lived, single-use authenticated callback. Thestatevalue is validated on receipt against the value originally stored by the plugin; mismatches cause the callback to be rejected. Tokens are not transmitted in URL query strings or any other channel that would persist them in webserver access logs or browser history.
During step 5, the tokens are held only in process memory on plugin.zportals.com for the duration of the exchange and the immediate handoff to your installation. They are not written to the ZPortals server's database, log files, or session storage. Once your WordPress installation receives them, they are encrypted at rest in zportals_auth on your own infrastructure.
Ongoing API traffic
After initial setup, every API call against Zoho is initiated directly from your WordPress installation using its own customer-specific tokens. Access tokens are refreshed by the plugin calling Zoho's token endpoint directly. plugin.zportals.com is not involved in any subsequent API traffic.
Storage properties
- Per-service compartmentalization. Each connected Zoho service (CRM, Books, Desk, Projects, Inventory, WorkDrive, Vault, Subscriptions, Sign) has its own row in
zportals_authso that revoking one connection does not affect the others. - Encrypted at rest. Access and refresh tokens are encrypted with AES-256-GCM keyed off the WordPress installation's authentication salt before being written to the database. Decryption happens only inside the request that needs the token.
- No tokens in error logs. Zoho OAuth tokens are excluded from the plugin's error log and from client responses.
Network architecture and transmission security
All outbound calls from the plugin to Zoho's APIs are made over HTTPS with TLS peer and hostname verification enabled. The plugin uses the WordPress HTTP API and curl, relying on the operating system's CA trust store to validate Zoho's certificate chain. Outbound calls from plugin.zportals.com to Zoho's APIs (for the OAuth handshake and for the ZPortals server's own subscription-management traffic) similarly use TLS with peer and hostname verification enabled. TLS verification is not disabled on any outbound code path in either codebase.
Your WordPress installation is responsible for terminating its own TLS connection. You must hold a valid TLS certificate on your site and enforce HTTPS at the WordPress layer (typically via FORCE_SSL_ADMIN in wp-config.php and a server-level HTTP-to-HTTPS redirect) so that all portal user traffic, including authentication cookies, is encrypted in transit. When the WordPress request context is HTTPS, ZPortals issues session and trusted-device cookies with the HttpOnly and Secure flags set so they cannot be read by client-side scripts or transmitted over plaintext connections.
Inbound network endpoints
The plugin exposes one REST route to the public internet:
POST /wp-json/zportals/v1/license-refresh
This route is called by the ZPortals license server when your subscription status changes. Every request must include:
- A timestamp within 5 minutes of the server clock (replay window).
- Your stored license key (constant-time
hash_equalscomparison). - An HMAC-SHA256 signature over the request payload, keyed by a secret derived from your license.
- A status payload in
{active, invalid}.
Any failure of any of those checks rejects the request with a 4xx response and logs the attempt. The route can be left open because the HMAC binding to your specific domain and license key prevents cross-customer or replay abuse.
No other endpoints are reachable without an authenticated session.
Administrative surface hardening
Plugin administrative surface (your WordPress)
Every administrative AJAX endpoint that performs a write or delete operation requires:
- A logged-in WordPress user with the
manage_optionscapability. - A valid
zp_admin_actionnonce (CSRF token), issued per session and verified withcheck_ajax_referer(). - Input validation appropriate to the operation: integer casts for ID arrays, allowlisted column names for sort operations,
wpdb->esc_likeforLIKEpatterns.
The same gate applies to form-based admin operations such as portal backup import: a wp_nonce_field is required on every form, and wp_verify_nonce is checked before any state change.
A separate zpAdminAjax JavaScript object is localized into every admin page so that all admin AJAX calls include the nonce automatically. The nonce is loaded in the page <head> so it is available before any inline script executes.
ZPortals server administrative surface (plugin.zportals.com)
The ZPortals server exposes administrative and customer-self-service interfaces for managing licenses, viewing API usage, and handling subscription billing. These interfaces enforce:
- Session-based authentication with bcrypt password hashing. Login forms never receive credential values from cookies or any other persistence layer.
- CSRF token validation on every state-changing form submission.
- Generic "invalid credentials" responses on failed login, so failed login attempts cannot be used to enumerate valid user accounts.
- Rate limiting on login attempts.
- Output encoding on every value rendered into HTML; request-derived parameters are never echoed without escaping.
Webhook endpoints that receive Zoho Subscriptions callbacks (license activation, renewal, cancellation) require a verifiable signature from Zoho on every request; unsigned or invalid-signature requests are rejected with a generic 4xx response.
License creation requires an authenticated session and is rate-limited per-IP to prevent abuse-driven license generation.
Server-side request forgery defenses
Features that fetch an administrator-configured URL on behalf of a portal user (currently the custom-button URL passthrough) route every request through a validating HTTP helper. The helper:
- Rejects any URL scheme other than
httporhttps. - Rejects URLs containing embedded credentials (
user:pass@host). - Resolves the target hostname and rejects the request if any returned address falls in a private, loopback, link-local, multicast, or otherwise reserved range (IPv4 and IPv6).
- Restricts the underlying transport to HTTP/HTTPS at the curl protocol level, blocking
file://,gopher://,dict://, and similar schemes. - Disables automatic redirect following; redirects are followed manually and every hop is re-validated against the same rules.
- Caps response size at 1 MiB and applies short connect and total timeouts.
- Logs rejected fetches to the plugin error log with a reason code, without echoing diagnostic detail to the caller.
This prevents a portal user from using the plugin as a probe for your internal network services or cloud-metadata endpoints.
SQL injection defenses
On your WordPress installation, ZPortals uses the WordPress $wpdb API for all database access. Queries that incorporate user-supplied data go through $wpdb->prepare() with %s and %d placeholders. Queries that build IN (...) clauses from arrays of IDs cast each value to an integer before assembly. LIKE patterns are wrapped with $wpdb->esc_like and bound through prepare.
On plugin.zportals.com, the server uses PDO with parameterized queries throughout. Every SQL query that incorporates a value from a request body, query string, or session uses bound parameters; no request-derived value is concatenated into a SQL string. This applies to the license validation gate, the customer/admin portal, and the subscription webhook handlers.
Allowlists are used in both codebases for any value that cannot be parameterized, such as ORDER BY column names and ASC/DESC direction tokens.
Update delivery
ZPortals plugin updates are delivered through a custom update server. All communication with the update server happens over HTTPS with TLS verification enabled in the update client. Administrators are notified of available updates through the standard WordPress plugin dashboard and install them through the standard WordPress plugin update flow. Customers should treat the ZPortals update server as a trust dependency, consistent with any plugin distribution mechanism.
Activity logging
Activity logging is disabled by default. When enabled by an administrator, the plugin records portal user actions such as record edits, including the previous and new values of tracked fields. Because the captured values may contain personally identifiable information, administrators should treat the zportals_activity_logs table as sensitive data and apply their own retention policies.
The activity log scope is configurable per integration (CRM, Books, Desk separately).
Data lifecycle and uninstall behavior
Removing the plugin through WordPress's standard uninstall flow runs uninstall.php, which:
- Drops all
zportals_*andzportalsinv_*tables. - Clears the cached license status on the ZPortals license server (so the seat is released).
- Removes plugin-specific options from
wp_options.
If the plugin is only deactivated (not uninstalled), data is preserved so reactivation restores prior state.
Per-user erasure
Administrators can delete an individual portal user from the Users admin panel. Deletion removes the row from zportals_users and, at the administrator's option, the corresponding WordPress user account. Activity log entries that reference the user's actions are retained by default for audit purposes, but the admin can clear them from the Activity Logs panel if customer policy requires it.
Subprocessors and data flow
ZPortals integrates with the following third-party services. Customer data crosses these boundaries when the corresponding feature is in use:
| Service | Purpose | Data sent |
|---|---|---|
| Zoho (CRM, Books, Desk, Projects, Inventory, WorkDrive, Vault, Subscriptions, Sign) | Primary integration; the system of record for portal data | All CRM record reads and writes triggered by portal users |
| Stripe | Payment processing for the optional e-commerce module | Payment intent metadata, customer email |
| Twilio | SMS delivery for 2FA codes (only if SMS 2FA is enabled) | Phone number, OTP code |
| Google reCAPTCHA | Bot detection on login and signup (only if enabled) | reCAPTCHA token, originating IP |
| Google Maps / Places | Address-field autocomplete in the CRM module (only if the Google integration is enabled by the administrator). Calls are issued directly from the portal user's browser to Google's APIs using the API key the administrator configures; this traffic does not transit ZPortals infrastructure | Partial address strings typed by the user |
ZPortals license server (plugin.zportals.com) | License validation, license management, OAuth callback handoff, plugin update delivery, subscription billing integration | Customer billing contact information, domain(s), license key, plugin version, theme version, active portal user count (a number only, not a list of users), API call counts per platform |
Hosting provider for plugin.zportals.com (LiquidWeb) | Underlying physical hosting, network, and storage for the ZPortals license server | All data listed for the ZPortals license server row above, persisted to the hosting provider's storage |
ZPortals does not subcontract data processing to any provider outside this list. CRM record content, attachments, and email message bodies are never transmitted to ZPortals-operated infrastructure; they flow directly between your WordPress installation and Zoho.
Privacy and regulatory posture
Because ZPortals is a self-hosted WordPress plugin, and CRM record content flows directly between your WordPress installation and Zoho, ZPortals does not process personal data of your portal end-users on ZPortals-operated infrastructure. For that data:
- You are the data controller under the GDPR and equivalent regimes.
- ZPortals functions as a software provider, not a data processor under Article 28 of the GDPR, in the same sense that a self-hosted plugin vendor is not a data processor for a site owner's user data.
Because the processing of portal user data occurs entirely within your own environment, a traditional Data Processing Agreement is not typically required for the ZPortals relationship. ZPortals can provide a written Data Processing Disclosure on request to satisfy your compliance team's documentation requirements.
For the limited B2B vendor-customer data that ZPortals does collect (described under "Data stored on plugin.zportals.com"), ZPortals applies the security controls described throughout this whitepaper: encrypted transmission, parameterized database access, authenticated administrative interfaces, and signed webhook callbacks.
For California residents, the same architecture applies under the CCPA: ZPortals does not sell personal information, does not share it with third parties for their independent marketing purposes, and does not process portal end-user data on its own infrastructure.
For EU/UK/Swiss customers whose use of the Service may involve cross-border transfer of B2B vendor-customer data to ZPortals-operated infrastructure outside the EEA, the applicable transfer mechanism is documented in the Data Processing Disclosure available on request.
Incident response and breach notification
If ZPortals detects a security incident that affects customer data held on ZPortals-operated infrastructure (plugin.zportals.com and its supporting systems), ZPortals will:
- Triage and contain the incident as a first priority.
- Notify affected customers without undue delay, and in any case within 72 hours of confirming that customer data was or may have been compromised, in line with the GDPR Article 33 notification standard.
- Provide regular updates as the investigation progresses, and a final post-incident summary covering the timeline, root cause, customer impact, and remediation steps taken.
Because portal end-user data resides on your WordPress installation and not on ZPortals-operated infrastructure, ZPortals does not have visibility into incidents affecting that data. You are responsible for monitoring and incident response on your own infrastructure. ZPortals will assist in good faith with any plugin-related questions that arise during your incident response.
Customer responsibilities
The security posture of a ZPortals deployment depends on the underlying WordPress and hosting environment. Customers are responsible for:
- A valid TLS certificate on the WordPress domain.
- Keeping WordPress core and other plugins up to date.
- Hardening the WordPress administrator account (strong password, ideally with a reputable WordPress-native 2FA plugin).
- Choosing a hosting provider that meets your compliance requirements.
- Layering a Web Application Firewall in front of the site if your threat model warrants one.
- Configuring secure backup of the WordPress database (including the
zportals_*tables). - Defining retention and erasure policies for activity logs and survey responses.
ZPortals provides hardening guidance for each of these in the ZPortals knowledge base.
Security review process
ZPortals conducts internal security reviews of every release before it ships. Reviews cover:
- All new and changed AJAX endpoints for authentication, authorization, and CSRF coverage.
- All new SQL queries for parameterization.
- All new file-handling code for path traversal and MIME validation.
- Third-party dependency updates for known CVEs.
The most recent comprehensive code audit was completed in 2026 Q2 and resulted in the security hardening described in this document.
A public changelog of security-relevant changes is published with every release.
Vulnerability disclosure
If you discover a security issue in ZPortals, please report it by emailing support@zportals.com. Reports should include enough detail to reproduce the issue and any disclosure timeline you would like us to follow.
We acknowledge reports within two business days. Our target mitigation timelines depend on severity: 7 days for critical issues that expose customer data or allow unauthorized access; 30 days for high-severity issues; 90 days for medium- or low-severity issues. We will credit reporters in the release notes unless they request otherwise.
Please do not publicly disclose unpatched vulnerabilities before contacting us. We treat all reports as confidential until a fix is released.
Found a security issue?
Email support@zportals.com. We acknowledge within two business days and aim to ship a fix within 30 days of confirmation.
Ready To Simplify Client Communication and Boost Your Productivity?

Book Your Demo
See exactly how ZPortals streamlines your workflow, enhances your client experience, and helps you close more deals - without the hassle. Our experts will personally walk you through it.

Start Your Free Trial
Prefer to dive right in? Try ZPortals risk-free for 30 days. No credit card, no confusion, no headaches. Just clear, measurable results.
