Inhoudsopgave
- Wat de REST API standaard blootstelt
- Gebruiker-enumeratie via /wp-json/wp/v2/users
- Het users-endpoint specifiek verbergen
- De hele REST API beperken tot ingelogde gebruikers
- Waarom je de REST API niet helemaal mag uitschakelen
- Application Passwords voor REST API-authenticatie
- Rate limiting van REST API-requests op serverniveau
- Security headers voor de REST API
- Wat dit artikel niet behandelt
- Wanneer hulp inschakelen
Wat de REST API standaard blootstelt
De WordPress REST API zit sinds versie 4.7 in de core (december 2016). Content die publiek is op je site, is ook publiek via de API. Dat geldt voor berichten, pagina's, reacties, categorieën, tags en gebruikers die content hebben gepubliceerd. Dat is geen bug. Het is hoe headless frontends, mobiele apps en de blokeditor data ophalen.
De standaard basis-URL is https://yoursite.nl/wp-json/wp/v2/. Open je die in een browser, dan krijg je een JSON-index van elke geregistreerde route. Geen authenticatie nodig. Welke routes er precies zijn hangt af van je geinstalleerde plugins, maar WordPress core registreert altijd endpoints voor posts, pages, media, users, comments, categories, tags en taxonomies.
De beveiligingsvraag is niet "is data toegankelijk?" (dat is het, by design) maar "is de verkeerde data toegankelijk?" Voor de meeste endpoints is het antwoord nee. Het users-endpoint is de uitzondering.
Gebruiker-enumeratie via /wp-json/wp/v2/users
Open https://yoursite.nl/wp-json/wp/v2/users in je browser. Als je geen beperkingen hebt ingesteld, krijg je een JSON-array met elke WordPress-gebruiker die minstens één bericht heeft gepubliceerd op een publiek posttype. Dit is het endpoint dat securityscanners markeren, en die markering is terecht.
De REST API Users-referentie documenteert precies welke velden terugkomen bij niet-geauthenticeerde requests (de view-context):
| Veld | Voorbeeldwaarde | Risiconiveau |
|---|---|---|
id |
1 |
Laag (intern database-ID) |
name |
Jorijn Schrijvershof |
Laag (weergavenaam, al zichtbaar op berichten) |
slug |
jorijn |
Hoog (afgeleid van user_login) |
url |
https://jorijn.com |
Laag (publieke profiel-URL) |
description |
WordPress developer |
Laag (publieke bio) |
link |
https://yoursite.nl/author/jorijn/ |
Laag (auteursarchief, al publiek) |
avatar_urls |
{"96":"https://secure.gravatar.com/..."} |
Laag (Gravatar-hash) |
Het veld dat ertoe doet is slug. Op de meeste WordPress-installaties is de slug identiek aan de user_login-waarde, tenzij iemand die handmatig heeft gewijzigd. Een aanvaller die slugs kan enumereren heeft een lijst van geldige inlognamen voor credential-stuffing-aanvallen op wp-login.php of xmlrpc.php.
Een veelvoorkomend misverstand is dat dit endpoint e-mailadressen lekt. Dat doet het niet. De velden email en username vereisen de edit-context, waarvoor authenticatie met de list_users-capability nodig is. Het werkelijke risico is de slug-naar-login equivalentie, niet e-mailblootstelling.
Dit gedrag zit in de core sinds WordPress 4.7. CVE-2017-5487 beschreef onvoldoende beperkingen op gebruikerslijsten in WordPress voor 4.7.1. De 4.7.1-patch beperkte het endpoint tot gebruikers met gepubliceerde berichten, maar voegde geen authenticatievereiste toe. Dat is nog steeds de stand van zaken in WordPress 6.7.
Het users-endpoint specifiek verbergen
Drie aanpakken, van minst naar meest ingrijpend. Kies er een.
Optie A: een leeg resultaat teruggeven voor niet-geauthenticeerde requests
Dit is de meest chirurgische fix. Elk ander REST-endpoint blijft werken en alleen gebruikersdata wordt verborgen voor niet-ingelogde bezoekers. Maak een bestand aan met de naam hide-rest-users.php in de map wp-content/mu-plugins/. Dat kan via de bestandsbeheerder van je hostingpaneel of via SFTP. Als de map mu-plugins nog niet bestaat, maak die dan eerst aan. Dit is een must-use plugin, dus het wordt automatisch geladen zonder activatie. Plak de volgende code in het bestand:
<?php
// Weiger niet-geauthenticeerde toegang tot het /wp/v2/users endpoint.
// Geauthenticeerde requests (Gutenberg, Application Passwords) gaan gewoon door.
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
// Match elk request naar /wp/v2/users of /wp/v2/users/<id>
if ( preg_match( '#^/wp/v2/users#', $request->get_route() ) && ! is_user_logged_in() ) {
return new WP_Error(
'rest_users_cannot_list',
'Gebruikersgegevens zijn niet beschikbaar.',
array( 'status' => 403 )
);
}
return $result;
}, 10, 3 );
Verwachte output na toepassing: curl -s https://yoursite.nl/wp-json/wp/v2/users | python3 -m json.tool geeft een JSON-object terug met "code": "rest_users_cannot_list" en HTTP-status 403. Ingelogde requests vanuit de blokeditor blijven gewoon werken, want Gutenberg authenticeert via WordPress-cookies.
Optie B: gebruik een plugin
Als je liever geen code schrijft: WP Cerber Security en Disable REST API bieden allebei granulaire endpointblokkering. WP Cerber laat je specifieke namespaces allowlisten terwijl je andere blokkeert. De Stop User Enumeration plugin richt zich specifiek op het blokkeren van ?author=N-queries en het REST users-endpoint.
Evalueer of de scope van de plugin past bij jouw probleem. Een plugin die alle ongeauthenticeerde REST-toegang blokkeert, breekt mogelijk publieke features die de REST API gebruiken (live zoeken, headless frontends, WooCommerce-winkelwageninteracties). Een plugin die alleen het users-endpoint blokkeert, is de juiste scope voor dit specifieke probleem.
Optie C: blokkeren op webserverniveau (nginx of Apache)
Als je SSH-toegang hebt: je kunt de blokkering afhandelen op webserverniveau, zodat het request PHP nooit bereikt. Dit is sneller maar minder flexibel: je kunt op deze laag geen onderscheid maken tussen geauthenticeerde en niet-geauthenticeerde requests. Beheer je je eigen webserver niet, gebruik dan Optie A of B.
# nginx: blokkeer /wp-json/wp/v2/users volledig
location ~* ^/wp-json/wp/v2/users {
deny all;
access_log off;
return 403;
}
# Apache: in .htaccess
RewriteEngine On
RewriteRule ^wp-json/wp/v2/users - [F,L]
Let op: dit blokkeert het endpoint voor iedereen, ook ingelogde administrators. De blokeditor haalt gebruikersdata op via dit endpoint voor het auteurskeuzeveld. Als je Gutenberg gebruikt en die dropdown breekt, schakel dan over naar Optie A, die alleen niet-geauthenticeerde requests blokkeert.
Je weet dat het werkt als curl -s -o /dev/null -w "%{http_code}" https://yoursite.nl/wp-json/wp/v2/users 403 teruggeeft in plaats van 200.
De hele REST API beperken tot ingelogde gebruikers
Als je site geen publieke content serveert via de REST API (geen headless frontend, geen publieke zoekwidget, geen WooCommerce-winkelwagen op de frontend), kun je authenticatie vereisen voor alle REST-requests. Dit is het canonieke patroon uit de REST API FAQ, met de rest_authentication_errors-hook (beschikbaar sinds WordPress 4.4). Maak een bestand require-rest-auth.php aan in wp-content/mu-plugins/ via de bestandsbeheerder van je hostingpaneel of via SFTP:
<?php
// Vereis authenticatie voor alle REST API-requests.
add_filter( 'rest_authentication_errors', function ( $result ) {
// Als een eerdere authenticatiecheck al gefaald is, geef die door.
if ( is_wp_error( $result ) ) {
return $result;
}
// Als een eerdere check al geslaagd is, geef die door.
if ( true === $result ) {
return $result;
}
// Nog geen authenticatie uitgevoerd. Vereis het.
if ( ! is_user_logged_in() ) {
return new WP_Error(
'rest_not_logged_in',
'Authenticatie is vereist.',
array( 'status' => 401 )
);
}
return $result;
} );
Dit is veilig voor Gutenberg. De blokeditor authenticeert via WordPress-sessiecookies. Als een beheerder is ingelogd en de editor opent, geeft is_user_logged_in() true terug en gaat elk REST-request vanuit de editor gewoon door.
Dit breekt het volgende, by design:
- Headless frontends die content ophalen via de REST API zonder authenticatie
- Publieke zoekwidgets die de REST API aansturen
- WooCommerce-winkelwagen, afrekenpagina en accountpagina's die ongeauthenticeerde REST-calls maken
- Elke externe integratie die publieke data leest van je API zonder credentials
- Het
?rest_route=-fallback-URL-formaat (dezelfde authenticatiecheck is van toepassing)
Als een van deze situaties op jouw site van toepassing is, gebruik dan de gerichte users-endpointblokkering uit de vorige sectie.
Verwachte output: curl -s https://yoursite.nl/wp-json/wp/v2/posts | python3 -m json.tool geeft "code": "rest_not_logged_in" terug met HTTP 401.
Waarom je de REST API niet helemaal mag uitschakelen
De REST API uitschakelen is iets anders dan authenticatie vereisen. "Uitschakelen" betekent een fout teruggeven voor alle REST-requests, inclusief die van ingelogde beheerders. Het REST API-handboek is er duidelijk over: "doing so will break WordPress Admin functionality that depends on the API being active."
De blokeditor (Gutenberg) is de grootste afhankelijkheid. Die haalt berichten, blokken, gebruikers, media en instellingen op via de REST API bij elke paginalading. Een gebruikersrapport in Gutenberg issue #9101 bevestigde het resultaat: een blanco wit scherm in de editor toen een securityplugin de REST API volledig uitschakelde. De oplossing was opnieuw inschakelen.
Andere corefuncties die afhankelijk zijn van de REST API: de sitegezondheidscheck (wp-admin > Hulpmiddelen > Sitegezondheid), de blokkendirectorybrowser, de plugin-/themazoekopdracht in het dashboard en het beheer van Application Passwords. De API uitschakelen breekt ze allemaal voor alle gebruikers, ook admins.
De juiste aanpak is altijd beperken, niet uitschakelen. Vereis authenticatie (de rest_authentication_errors-hook hierboven) of blokkeer specifieke endpoints. Beide houden de API werkend voor ingelogde gebruikers terwijl ze ongeauthenticeerde toegang weigeren.
Application Passwords voor REST API-authenticatie
Application Passwords zijn toegevoegd in WordPress 5.6 (december 2020) en zijn de aanbevolen authenticatiemethode voor programmatische REST API-toegang.
Elk Application Password is een alfanumerieke token van 24 tekens met meer dan 142 bits entropie. Ze worden opgeslagen als gehashte waarden in gebruikersmetadata, zijn individueel intrekbaar en houden de laatst-gebruikte timestamp bij (nauwkeurig tot op 24 uur). Ze werken met de REST API en XML-RPC, maar kunnen niet worden gebruikt op wp-login.php. De rechten van een Application Password worden overgenomen van de gebruiker die het heeft aangemaakt.
Hoe je er een aanmaakt. Ga naar wp-admin > Gebruikers > Profiel > Application Passwords, voer een naam in voor de integratie (bijvoorbeeld "CI deploy-script" of "iOS-app") en klik op "Nieuw application password toevoegen". WordPress genereert het wachtwoord eenmalig. Kopieer het direct; je kunt het later niet meer ophalen.
Hoe je het gebruikt. Stuur het mee als HTTP Basic Auth met de gebruikersnaam en het Application Password (spaties in het wachtwoord zijn cosmetisch en worden genegeerd):
# Berichten ophalen met authenticatie via Application Password
curl -u "jorijn:ABCD 1234 EFGH 5678 IJKL 9012" \
https://yoursite.nl/wp-json/wp/v2/posts?context=edit
HTTPS is standaard vereist. WordPress weigert Application Password-authenticatie via gewoon HTTP, tenzij je het expliciet overschrijft met add_filter( 'wp_is_application_passwords_available', '__return_true' ). Doe dat niet. De credential gaat in cleartext over HTTP; dat verslaat het hele doel.
Een misverstand dat even opgehelderd mag worden: Application Passwords maken de REST API niet onveiliger. De officiële WP 5.6-communicatie stelt rechtstreeks dat ze "de REST API niet blootstellen aan nieuwe kwetsbaarheden." Voor Application Passwords bestonden, gebruikten ontwikkelaars de legacy Basic Auth-plugin (die WordPress zelf bestempelt als "should only be used for development and testing") of deelden ze hun hoofdwachtwoord. Application Passwords zijn de gestructureerde, intrekbare, controleerbare vervanging.
Rate limiting van REST API-requests op serverniveau
Dezelfde rate-limitingpatronen uit het brute force-beschermingsartikel gelden ook voor de REST API. De REST API is een extra aanvalsoppervlak voor credential stuffing (via Application Passwords of cookie-gebaseerde auth) en voor resource-uitputting (dure queries tegen /wp/v2/posts?per_page=100&_embed).
Als je site achter Cloudflare zit, maak dan een rate-limitingregel aan in het Cloudflare-dashboard onder Security > Rate Limiting Rules voor pad /wp-json/ met een drempel die past bij je verkeer (begin met 120 requests per minuut per IP en scherp aan van daaruit). Veel managed WordPress-hosts bieden ook ingebouwde rate limiting via hun controlepaneel.
Als je SSH-toegang hebt en je eigen nginx-configuratie beheert, voeg dan een rate-limitzone toe voor het REST API-pad:
# In het http {} blok
limit_req_zone $binary_remote_addr zone=restapi:10m rate=60r/m;
# In het server {} blok
location /wp-json/ {
limit_req zone=restapi burst=30 nodelay;
try_files $uri $uri/ /index.php?$args;
}
Zestig requests per minuut per IP met een burst van dertig is redelijk voor de meeste sites. Headless frontends of mobiele apps die veel API-calls maken, hebben misschien een hogere limiet nodig. Stem af op basis van je access logs.
Security headers voor de REST API
REST API-responses erven de security headers die je webserver globaal instelt. De headers die het meest van belang zijn voor API-responses:
X-Content-Type-Options: nosniffvoorkomt dat browsers JSON-responses MIME-sniffen als HTML. WordPress zet deze standaard op REST-responses sinds 4.7.X-Frame-Options: DENYofSAMEORIGINvoorkomt het embedden van API-responses in iframes. Strikt genomen niet nodig voor JSON, maar goede hygiëne.Cache-Control: no-store, privateop geauthenticeerde responses voorkomt dat proxy's gepersonaliseerde data cachen. WordPress regelt dit voor responses metedit-context.
De meeste managed WordPress-hosts en Cloudflare zetten deze headers standaard. Je kunt het controleren via de ontwikkelaarstools van je browser: open het tabblad Netwerk, laad https://yoursite.nl/wp-json/wp/v2/ en bekijk de responseheaders.
Als je SSH-toegang hebt en je eigen nginx-configuratie beheert, voeg de headers dan handmatig toe als ze ontbreken:
location /wp-json/ {
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# ... overige directives
}
Wat dit artikel niet behandelt
- Custom REST API-endpoints geregistreerd door plugins of thema's. Die endpoints hebben hun eigen
permission_callback(verplicht sinds WordPress 5.5, dat een_doing_it_wrong-melding geeft als die ontbreekt). Het auditen van custom endpoints is een plugin-voor-plugin-taak. - De
?author=Nquerystring-enumeratie. Dit is een apart, ouder enumeratievector dat dateert van voor de REST API. De Stop User Enumeration plugin dekt beide vectoren. - WAF-regelsets voor de REST API (ModSecurity, Cloudflare managed rules). Die zijn infrastructuurspecifiek en variëren per provider.
- OAuth 2.0- en JWT-authenticatie. Dit zijn authenticatiemethoden die plugins leveren (bijvoorbeeld WP OAuth Server) en die buiten de WordPress-core vallen. De hardening-checklist behandelt de authenticatiebasis.
Wanneer hulp inschakelen
Verzamel het volgende voordat je hulp vraagt:
- De WordPress-versie, PHP-versie en webserver (nginx of Apache)
- De output van
curl -s https://yoursite.nl/wp-json/wp/v2/users | head -50(wat het endpoint nu teruggeeft) - Welke REST API-beperkingsmethode je hebt toegepast en waar (mu-plugin, pluginnaam, webserverconfiguratie)
- Of de blokeditor (Gutenberg) nog werkt na de wijziging
- Een lijst van plugins die afhankelijk zijn van de REST API (WooCommerce, headless thema's, zoekwidgets)
- Het beveiligingsscannerrapport dat het endpoint heeft gemarkeerd (Wordfence, WPScan, Acunetix of andere)
Schakel hulp in wanneer:
- Je de
rest_authentication_errors-filter hebt toegepast en de blokeditor een blanco scherm toont. Iets blokkeert dan ook geauthenticeerde requests. Controleer of geen andere plugin eenWP_Errorteruggeeft voordat die van jou draait. - Je WooCommerce-afrekenpagina brak na het beperken van de API. WooCommerce gebruikt ongeauthenticeerde REST-endpoints voor de winkelwagen. Je hebt endpointniveau-blokkering nodig (alleen users), niet API-brede auth.
- Een penetratietest nog steeds gebruikerenumeratie markeert nadat je
wp-json/wp/v2/usershebt geblokkeerd. Controleer de?author=N-vector:curl -s -o /dev/null -w "%{http_code}" https://yoursite.nl/?author=1zou 403 moeten teruggeven of redirecten naar een niet-informatieve pagina. - Je geauthenticeerde REST API-requests ziet van IPs die je niet herkent. Controleer
wp-admin > Gebruikers > Profiel > Application Passwordsvoor elke admingebruiker. Trek wachtwoorden in die je niet herkent. Als je een gecompromitteerd account vermoedt, behandelt het artikel over gehackte WordPress-site met malware-redirect de incidentrespons.