Security model

Giving an AI assistant access to a live site is only reasonable if every request is authenticated, limited to what you allowed, slowed down when it misbehaves, reversible where possible, and written down. This is how plugin 1.7.0 does each of those, and where its limits are.

On this page

Every request, in order

  1. HTTPS: plain-HTTP requests are refused with 426.
  2. Token: signature, expiry, issuer and revocation are checked.
  3. IP allow-list, if the operator saved one.
  4. Scope: the token must list the scope the route needs.
  5. Capability: the WordPress user behind the token must hold the capability the route needs. Every token route checks one.
  6. Rate limit: per token and per signed-in administrator, tighter on sensitive scopes.
  7. Confirmation: destructive operations stop and ask for a second, explicit call.
  8. Guards: SQL, file paths and remote downloads are checked before anything runs.
  9. Audit: the request and its result are written to the audit log.

Tokens and the signing secret

  • Tokens are JSON Web Tokens signed with HS256. The key is WPCB_JWT_SECRET, defined in wp-config.php and never stored in the database; it must be at least 32 characters, and the plugin suggests 64.
  • A token carries its issuer (your site address), issue time, expiry, a unique ID, the WordPress user ID it belongs to, and its scopes. Every request checks the signature and expiry, that the issuer is this site, and that the ID is not revoked.
  • Tokens expire after 1 to 365 days (30 by default). Rotate secret in Xaflo → Settings invalidates every token at once.
  • After updating to 1.7.0, rotating is required: earlier versions let a read-only administrator token read wp-config.php. See Upgrading to 1.7.0.
  • The token is shown once, when you create it. Give each client or person its own token, so you can revoke one without touching the others.

Scopes, capabilities and what is administrator-equivalent

Scopes say what a token may ask for; WordPress capabilities say what its user may do. Every route checks both, so a token can never exceed its owner: a token made by an Editor cannot install plugins even if it lists plugin:install. File, maintenance, backup, audit, database and WP-CLI routes require an administrator.

Some scopes give the token holder an administrator’s power, whatever the guards below do: fs:admin, db:write, cli:exec, system:backup, plugin:write, theme:write and admin:login. Treat a token with any of them like an administrator password, and keep it out of everyday use. All scopes are listed in Token scopes.

HTTPS only

The plugin refuses requests over plain HTTP with 426 wpcb_upgrade_required, so tokens never travel unencrypted. Local development hosts are exempt: localhost, 127.0.0.1, ::1 and names ending in .localhost, .test or .local, judged from the site’s own address.

Rate limits

Each token can make 60 requests per minute by default, counted with a sliding window; administrators signed in to wp-admin are counted the same way. Sensitive scopes have their own, lower limits: 5 per minute for cli:exec and system:backup, 10 for db:write and plugin:install, 20 for theme and plugin file writes, 30 for media writes and publishing. Above the limit the API answers 429 with Retry-After.

Two-step confirmation

Operations that destroy data, or could lock you out, do not run on the first call. They answer 428 with a summary of what would happen and a one-time confirmation token; only a second call that carries that token runs the operation. The token expires after five minutes and is bound to the operation, the user and, for writes, the exact content.

Operations that ask for confirmation:

  • SQL writes (INSERT, UPDATE, DELETE);
  • restoring a backup;
  • deleting a post or page permanently, and deleting media;
  • installing a plugin from a zip or the WordPress.org directory, and replacing an installed plugin with an uploaded zip;
  • switching theme;
  • changing the site address (siteurl, home) or the admin email;
  • with fs:admin: every overwrite, rename and delete, and every write of an executable or server-config file.

Confirmations guard against accidents, not against the token holder. Whoever holds the token can request a confirmation and use it in two automated calls. What makes the difference is who answers: Xaflo Desktop 2.0 and MCP server 1.3 always show the change to you and wait for your yes; they never confirm by themselves.

Backups and undo

A full backup is taken only when you ask for one; the other protections are smaller and automatic:

  • Post and page updates save a WordPress revision before they change anything.
  • Overwriting a file through the file routes keeps the previous version in the plugin’s backup folder and returns its backup id; theme and plugin file writes keep the previous version too.
  • wp_backup_create makes a full backup (database, files or both) as one zip in wp-content/wpcb-backups/; wp_backup_restore brings it back, with confirmation. Restores run in a transaction, never bring back the bridge’s own folder, tokens or settings, and restore users and roles as they were in the backup.
  • The plugin blocks web access to the backup folder on Apache. On nginx, add a rule yourself:
nginx
location ~ /wpcb-backups/ { deny all; }

Files, SQL and downloads

Files

  • Reading needs site:read and an administrator. Secrets are refused and hidden from listings and search: anything named like wp-config, .env files, *.bak* files, SQL dumps, debug.log, .user.ini, php.ini, web.config, .htpasswd, and anything under wpcb-backups/, .git/ or .svn/, checked on the requested and the resolved path.
  • Writing with site:write works strictly inside the uploads folder, on inert types only (documents, images, audio, video, archives). Writes never go through a symbolic link, and : is refused in any path.
  • Writing with fs:admin reaches any file under the site root except the guardrails: protected names, the site root itself, the bridge’s own folder, the backup folder and any folder containing them.

SQL

  • /db/query needs an administrator. One statement per call, every table must carry your site’s prefix, and only SELECT, INSERT, UPDATE and DELETE are accepted; schema changes, other databases, WITH, executable comments, file access and timing functions are refused. Unbounded SELECT results are capped at 1000 rows.
  • Password hashes are removed from every result.
  • Writes need InnoDB and run in a transaction. If users, roles, the plugin’s tokens or protected options (site address, admin email, active plugins, theme and more) changed, the statement is rolled back.
  • The route is refused entirely under SQL modes or connection character sets where its checks would not be reliable.

WP-CLI

Off on every site until the owner enables it. When on, a command must match an allow-list entry exactly, flags are allowed per command, code-loading flags are refused, shell metacharacters are rejected, every argument is escaped, and each run stops after 30 seconds with at most 16 KB of output.

Remote downloads

When the plugin fetches something on your behalf (an image from a URL, a plugin zip, a link check), the request goes through an SSRF guard that checks the destination first, with TLS verification.

Quick login

A token with admin:login, owned by an administrator, can ask for a one-time link that signs that same user in to wp-admin. The link’s secret is 256 random bits, stored only as a hash, valid 60 seconds and usable once. It stops working when the token is revoked or expires, when the secret is rotated, when the user loses administrator rights, and when the browser’s address fails the IP allow-list. It is refused without HTTPS and while a known two-factor sign-in plugin is active, because the link would skip the second step. Issue and use are audited; the link itself is never logged. The MCP server does not offer it to models on purpose.

Audit log

Every request is recorded once, refusals included, with time, token ID, user, route, method, the scope it needed, the result and how long it took. Passwords and keys are redacted; file bodies, CSS, SQL values and CLI arguments are replaced by their size; URLs are stored without credentials or query strings. Read it in Xaflo → Audit Log. Entries are kept 90 days by default (1 to 3650), and a daily job removes older entries and expired tokens.

The client IP is the real one: the rightmost untrusted X-Forwarded-For hop, trusted only from proxies the operator lists; CF-Connecting-IP is not trusted.

Stated limits

From the 1.7.0 release notes, so you can decide what to grant:

  • fs:admin can write a new, ordinarily named file anywhere outside the guardrails without confirmation, so it can place code in one call.
  • db:write can still change most tables; the SQL guard is a filter on the request, not a full SQL parser.
  • cli:exec can read every option, and a backup restore brings back users and roles as they were.
  • Requests to the installer routes and the maintenance preview are not yet written to the audit log.

What the plugin sends out

Nothing on its own. It contacts no Xaflo service and sends no telemetry. The self-hosted build checks this site for updates and installs a release only if the manifest has a SHA-256, the download is https on xaflo.com, and the zip matches the hash. Everything else is a response to a request you, or your assistant with your token, made.

Reporting a security issue

Write privately to security@xaflo.com. Please do not open a public issue. The site’s /.well-known/security.txt has the same contact.