Security
Security Model
ReactWP combines WordPress permissions with a restrictive REST gate. It does not make every WordPress REST endpoint public simply because the frontend uses React.
Treat three cases separately:
- public ReactWP content endpoints
- authenticated WordPress REST requests
- project-specific private endpoints
REST Gate
Routes listed by rwp_allowed_rest_routes bypass the default ReactWP block. The built-in ReactWP endpoints are added there by the runtime.
For every other REST request:
- an authenticated administrator with a valid WordPress REST session can continue
- a browser that merely has an admin account but sends no valid cookies/nonce is not authenticated
- guests and non-admin users are blocked unless the route is explicitly allowed
An external request does not gain admin access from the username alone. Cookie requests must use credentials: 'include', and protected WordPress REST requests normally need the current X-WP-Nonce.
Public Project Routes
Allow only the exact project route that must be public:
add_filter('rwp_allowed_rest_routes', function($routes){
$routes[] = '/my-project/v1/contact';
return $routes;
});
The allowlist only lets the request reach its REST registration. Your permission_callback still owns authorization.
register_rest_route('my-project/v1', '/account', [
'methods' => 'GET',
'permission_callback' => function(){
return current_user_can('read');
},
'callback' => 'my_project_account_payload',
]);
Do not use __return_true for private data.
Public Content Visibility
The public bootstrap and route endpoints resolve only posts that WordPress considers publicly viewable. ReactWP also excludes password-protected content from public payloads. Drafts, private posts, scheduled posts, trashed posts, and non-public post types return the normalized 404 route instead of their ACF or SEO data.
Authentication does not silently widen those public endpoints. Use the signed preview endpoint for unpublished editorial previews, and use a project-specific endpoint with its own permission_callback for private application data.
Protecting a WordPress Page
A page can remain published while its route payload is conditional. Enforce the condition in PHP, not only by hiding the React component.
For example, a route filter can replace protected data for unauthorized visitors, while a private REST endpoint can use a capability check. Client-side redirects are user experience; server-side permission checks are the security boundary.
Never include a secret in route.data, the bootstrap payload, public settings, or rendered HTML and then rely on CSS or React to hide it.
Headless Authentication
ReactWP provides:
POST /reactwp/v1/auth/loginGET /reactwp/v1/auth/mePOST /reactwp/v1/auth/logout
The login endpoint is rate-limited and returns a generic failure message. Auth and preview responses use no-store headers. Credentialed origins must be explicitly allowlisted and should use HTTPS outside local development.
The public bootstrap never contains currentUser. Read identity and the REST nonce from /auth/me, which is intentionally separated from cacheable public content.
Use the restNonce returned after login for later authenticated WordPress REST calls. See Headless API.
CORS
Allowed headless origins come from the site URLs, Site settings > Headless API, and rwp_headless_allowed_origins.
ReactWP ignores wildcard origins for authenticated headless access. Register the exact scheme, host, and port:
https://app.example.com
http://localhost:3000
The rwp_headless_allow_insecure_auth filter exists as an escape hatch, but production credential flows should use HTTPS instead.
Preview Tokens
Create a signed preview token on the server:
$token = rwp::preview_token($post_id, 600);
Tokens are scoped to one post, expire, and are signed with WordPress salts. They are credentials: do not log them, store them permanently, or expose them to unrelated origins.
Sanitizing and Escaping
Sanitize untrusted input when accepting it and escape output for its final context.
$email = rwp::sanitize('email', [
'value' => $_POST['email'] ?? ''
]);
echo rwp::escape('attr', $email);
For AJAX or REST forms also use:
- a nonce where the request comes from your own frontend
- strict server-side validation
- capability checks for privileged actions
- MIME and extension validation for uploads
- WordPress upload APIs instead of trusting the client filename
- size and file-count limits
Frontend validation improves feedback but does not replace PHP validation.
The bundled SVG plugin sanitizes uploads with enshrined/svg-sanitize, removes executable markup and event attributes, permits only same-document href references, and rewrites the temporary upload with the cleaned XML before WordPress stores it. The sanitizer library is shipped with the plugin and audited through Composer.
Public Payload Review
Before adding data through rwp_bootstrap, rwp_route_payload, or rwp_headless_public_settings, assume every visitor can read it. Keep API secrets, SMTP credentials, private user fields, server paths, and internal tokens out of public payloads.