Proxy API et sécurité
Ton WordPress ne doit jamais être accessible directement. Jamais.
8.1Pourquoi un proxy ?
Sans proxy, ton front appelle directement WordPress :
# MAUVAIS : le navigateur appelle directement WordPress
Browser → https://cms.monsite.fr/wp-json/wp/v2/posts
↑
L'URL WordPress est exposée dans le DevTools
Navigateur, attaques ciblées, robots...Avec un proxy, tout passe par ton domaine :
# BON : le navigateur appelle ton propre domaine
Browser → https://www.monsite.fr/api/wp-json/wp/v2/posts
↓
Proxy (Cloudflare/Next.js)
↓
https://cms.monsite.fr/wp-json/wp/v2/posts
↑
URL invisible pour le visiteurLes avantages :
- Sécurité : l'URL WordPress n'apparaît nulle part côté client.
- Pas de CORS : le navigateur appelle le même domaine, donc pas de cross-origin.
- Cache : tu peux cacher les réponses au niveau du proxy (CDN edge).
- Contrôle : tu peux filtrer, limiter ou transformer les requêtes avant qu'elles atteignent WordPress.
8.2Proxy avec Next.js rewrites (développement)
En développement local, Next.js peut jouer le rôle de proxy grâce aux rewrites :
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/api/wp-json/:path*',
destination: 'http://mon-cms-headless.local/wp-json/:path*',
},
]
},
}
export default nextConfigMaintenant, depuis ton code Next.js, tu appelles /api/wp-json/... et Next.js reroute vers WordPress de manière transparente.
8.3Proxy avec Cloudflare Pages Functions (production)
En production, les rewrites de Next.js ne suffisent pas si tu héberges sur Cloudflare Pages. Il te faut une Pages Function qui fait le proxy :
// functions/api/[[path]].ts
// Catch-all : intercepte toutes les requêtes /api/*
interface Env {
WP_API_ORIGIN: string // https://cms.monsite.fr
}
export const onRequest: PagesFunction<Env> = async (context) => {
const url = new URL(context.request.url)
// Reconstruit l'URL WordPress
const wpPath = url.pathname.replace(/^\/api/, '')
const wpUrl = `${context.env.WP_API_ORIGIN}${wpPath}${url.search}`
// Transmet la requête avec les cookies
const wpResponse = await fetch(wpUrl, {
method: context.request.method,
headers: context.request.headers,
body: context.request.method !== 'GET'
? await context.request.text()
: undefined,
})
// Renvoie la réponse au navigateur
const response = new Response(wpResponse.body, {
status: wpResponse.status,
headers: wpResponse.headers,
})
return response
}8.4Protéger WordPress côté serveur
Le proxy cache l'URL, mais si quelqu'un la découvre, WordPress est toujours accessible. Ajoute une couche de protection côté serveur :
# .htaccess (Apache / o2switch)
# Bloque l'accès direct à wp-json sauf depuis le proxy
<IfModule mod_rewrite.c>
RewriteEngine On
# Autorise les requêtes depuis le proxy Cloudflare
# et les requêtes internes (wp-admin)
RewriteCond %{HTTP:X-Forwarded-For} !^(104\.16\.|172\.67\.)
RewriteCond %{REQUEST_URI} ^/wp-json/
RewriteCond %{HTTP_REFERER} !^https://cms\.monsite\.fr
RewriteRule ^ - [F,L]
</IfModule>En complément, limite l'accès à wp-admin par IP ou par authentification HTTP :
<?php
// Dans un plugin : restreindre l'API REST aux origines autorisées
add_filter('rest_authentication_errors', function($result) {
if (!empty($result)) {
return $result;
}
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$referer = $_SERVER['HTTP_REFERER'] ?? '';
$allowed = [
'https://www.monsite.fr',
'https://cms.monsite.fr',
];
// En production, vérifie l'origine
$is_allowed = false;
foreach ($allowed as $domain) {
if (str_starts_with($origin, $domain) ||
str_starts_with($referer, $domain)) {
$is_allowed = true;
break;
}
}
// Autorise toujours les requêtes internes (cron, CLI)
if (defined('REST_REQUEST') && !$is_allowed && php_sapi_name() !== 'cli') {
return new WP_Error(
'rest_forbidden',
'Accès non autorisé.',
['status' => 403]
);
}
return $result;
});8.5Checklist sécurité headless
NEXT_PUBLIC_)/api/wp-json/...)*)HttpOnly, Secure, SameSite=Nonepermission_callback