User enumeration WordPress voorkomen is een van die dingen waar iedereen “moet doen” zegt en niemand écht doet. Het is niet dat je site meteen gehacked wordt zodra usernames uitlekken. Een gehackte site is vervelend, geen ramp. Maar het is stap 1 van een brute-force-keten: wie je usernames kent, hoeft alleen nog een wachtwoord te raden. Ik krijg regelmatig de vraag na een security-scan: “Waarom laat mijn site zomaar alle usernames zien?” Het antwoord is meestal: omdat WordPress dat standaard doet.
Dit artikel loopt de zes klassieke lekken langs, met concrete fixes en een curl-test waarmee je zelf kunt controleren of het nog lekt. Plus een eerlijke afweging tussen plugin en custom code.

Wat user enumeration eigenlijk is en waarom het jou raakt
User enumeration is het geautomatiseerd verzamelen van geldige gebruikersnamen op een site. WordPress verklapt op meerdere plekken welke usernames bestaan. Als een aanvaller weet dat “admin”, “sander” en “info” actieve accounts zijn, kan hij daar dag en nacht wachtwoorden op proberen. Rate-limiting helpt (Fail2Ban, Wordfence, Cloudflare WAF), maar een lek dat traag traceert blijft een lek.
Praktisch: user enumeration is geen kritiek probleem op zichzelf, wel de opmaat naar credential-stuffing en brute-force. Op een site met sterke wachtwoorden en 2FA overleeft je login-formulier het meestal. Op een site met “welkom01” als admin-wachtwoord: één weekend werk voor een botnet.
User enumeration WordPress voorkomen: de 6 lekken

?author=1 redirect
De klassieker. https://site.nl/?author=1 redirect standaard naar /author/gebruikersnaam/ en toont daarmee de username van user-id 1. Aanvallers loopen dit door van 1 tot 20 en hebben binnen 10 seconden alle admin- en editor-accounts.
Fix: in nginx of Apache een 404 teruggeven op requests met ?author= in de querystring, of via een mu-plugin die het request afvangt en 301 naar de homepage stuurt:
add_action( 'template_redirect', function() {
if ( isset( $_GET['author'] ) ) {
// Fix: 301 naar homepage (voor een strikte 404 vervang je dit door status_header(404); nocache_headers(); exit;).
wp_safe_redirect( home_url(), 301 );
exit;
}
}, 1 );REST API /wp/v2/users
De REST-endpoint /wp-json/wp/v2/users geeft standaard, zonder authenticatie, een JSON-array met alle users die minimaal één gepubliceerde post hebben. Voor blogs is dat je hele redactie. Voor WooCommerce-sites met author-metaboxes op productpagina’s: idem.
Fix: filter rest_endpoints om de /users-route te verwijderen voor unauthenticated requests, of installeer een plugin die dit doet (Disable REST API, Stop User Enumeration). WPS Hide Login verplaatst alleen /wp-login.php en dicht dit REST-endpoint dus niet.
add_filter( 'rest_endpoints', function( $endpoints ) {
if ( ! is_user_logged_in() ) {
unset( $endpoints['/wp/v2/users'] );
unset( $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
}
return $endpoints;
} );Author-archives en author-slug
Zelfs zonder ?author-parameter verklapt /author/slug/ de username, want WordPress gebruikt standaard user_login als user_nicename (de URL-slug). Twee fixes, afhankelijk van hoe hard je wilt gaan:
- Author-archives volledig uitzetten: 404 op alle
/author/*URL’s via een filter of via je thema’sfunctions.php. - Author-slug loskoppelen van username: update
user_nicenamein de database naar de display-name (of een random slug). WordPress toont dan de nicename in URL’s, niet je login.
Voor sites met bekende auteurs (blogs, corporate) is loskoppelen praktischer dan uitzetten.
oEmbed-endpoint
https://site.nl/wp-json/oembed/1.0/embed?url=[postURL] retourneert author_name (display-name, geen login). Op zichzelf niet kritiek, want display-name is publiek zichtbaar in posts. In combinatie met ?author=1 bevestigt het welke user-id bij welke display-name hoort.
Fix: filter oembed_response_data om author_name en author_url te strippen. Alleen relevant als je author-slug al hebt losgekoppeld.
Login-error verschillen (invalid username vs password)
WordPress geeft standaard verschillende foutmeldingen: “Onbekende gebruikersnaam” versus “Wachtwoord onjuist”. Aanvallers gebruiken dat verschil om usernames te bevestigen.
Fix: filter login_errors en geef altijd een generieke melding.
add_filter( 'login_errors', function() {
return __( 'Inloggegevens onjuist.', 'lypii' );
} );XML-RPC en sitemap
XML-RPC (/xmlrpc.php) heeft methodes zoals wp.getUsersBlogs en system.multicall die usernames kunnen bevestigen. Sinds WordPress 5.5 is er ook /wp-sitemap-users-1.xml, een publieke lijst van user-URL’s.
Fix:
- XML-RPC volledig uitzetten als je geen mobile-app of Jetpack-koppeling gebruikt (
add_filter( 'xmlrpc_enabled', '__return_false' );). - Users-sitemap uitzetten:
add_filter( 'wp_sitemaps_add_provider', function( $provider, $name ) { return $name === 'users' ? false : $provider; }, 10, 2 );.
WordPress user-enumeration check
Test zes plekken die gebruikersnamen kunnen verklikken
Vul je eigen domein in, test elk lek rustig en markeer het pas als gedicht nadat je opnieuw hebt getest.
De testopdrachten worden alleen als tekst aangepast. Dit blok voert niets uit.
Gebruik de curl-opdrachten alleen tegen een site die je zelf beheert. Test loginfouten eenmalig en houd rekening met je eigen brute-force-limiet.
1. ?author=1 redirect
Waar let je op? Een redirect met een gebruikersslug in de Location-header verklikt een accountnaam.
add_action( 'template_redirect', function () {
if ( isset( $_GET['author'] ) ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
} );2. REST API users-endpoint
Waar let je op? Een openbare JSON-lijst met namen, slugs of auteursgegevens.
add_filter( 'rest_endpoints', function ( $endpoints ) {
if ( ! is_user_logged_in() ) {
foreach ( array_keys( $endpoints ) as $route ) {
if ( strpos( $route, '/wp/v2/users' ) === 0 ) {
unset( $endpoints[ $route ] );
}
}
}
return $endpoints;
} );3. Author-archive en slug
Waar let je op? Een bestaande author-pagina bevestigt de slug, ook als de queryredirect al dicht is.
add_action( 'template_redirect', function () {
if ( is_author() ) {
wp_safe_redirect( home_url( '/' ), 301 );
exit;
}
} );4. oEmbed author-gegevens
Waar let je op? Velden als author_name en author_url in het oEmbed-antwoord.
add_filter( 'oembed_response_data', function ( $data ) {
unset( $data['author_name'] );
unset( $data['author_url'] );
return $data;
} );5. Verschillende login-error messages
Waar let je op? Verschillende antwoorden voor een onbekende en een bestaande testgebruiker. Voer beide tests niet herhaald uit.
add_filter( 'login_errors', function () {
return 'Inloggen mislukt.';
} );6. XML-RPC en users-sitemap
Waar let je op? Een bereikbare XML-RPC-route en een sitemap met author-URL’s. Controleer ook een SEO-plugin die een eigen author-sitemap maakt.
add_filter( 'xmlrpc_enabled', '__return_false' );
add_filter( 'wp_sitemaps_add_provider', function ( $provider, $name ) {
if ( 'users' === $name ) {
return false;
}
return $provider;
}, 10, 2 );De xmlrpc_enabled-filter schakelt alleen XML-RPC-methoden uit die authenticatie nodig hebben. Voor een volledige blokkade heb je een server- of WAF-regel nodig. Test eerst of een koppeling XML-RPC gebruikt.
Voortgang
0/6 gedicht
0/6 gecontroleerd
Nog open volgens je checklist
Alle zes punten staan nog open.
Wat je echt moet doen: kies code, een goed onderhouden beveiligingsplugin of beide, maar test ieder endpoint afzonderlijk.
Alleen de login-URL verplaatsen sluit het REST users-endpoint, author-archief en oEmbed niet af.
Gebruik een WAF, beperk brute-force-pogingen en zet 2FA aan voor accounts met rechten. Dit vervangt de zes controles niet, maar beperkt het risico als een gebruikersnaam toch bekend wordt.
Test na elke fix opnieuw. Eén vergeten endpoint verklikt alsnog je gebruikersnamen.
Wat je écht moet doen (code, plugin of allebei)
Twee routes, afhankelijk van je situatie:
Optie A: quick-win plugin. Installeer Stop User Enumeration op WordPress.org plus een WAF (Wordfence, Sucuri, of Cloudflare). Kost 15 minuten en dekt naar schatting 70% van de lekken. Voor sites zonder gevoelige data, kleine blogs of persoonlijke sites is dit ruim voldoende.
Optie B: grondig via mu-plugin. Een custom mu-plugin met de zes filters hierboven, plus author-slug loskoppeling in de database. Kost 1 tot 2 uur en dekt ongeveer 95% van de lekken. In mijn ervaring is optie B beter voor sites met bekende auteurs, WooCommerce-shops en klantenportalen, waar het lek-oppervlak groter is.
Combineren mag: mu-plugin voor de zes filters, plus Wordfence als brute-force-vangnet.
Test of je site nog lekt

Vijf curl-commando’s die je in twee minuten laten zien of je nog lekt.
# 1. ?author-redirect
curl -I "https://site.nl/?author=1"
# Verwacht: 404 of 301 naar de homepage. Zie je 301 naar /author/xxx/, dan lek je.
# 2. REST users-endpoint
curl "https://site.nl/wp-json/wp/v2/users"
# Verwacht: 401 of lege array. Zie je een JSON-array met users, dan lek je.
# 3. Users-sitemap
curl -I "https://site.nl/wp-sitemap-users-1.xml"
# Verwacht: 404.
# 4. Author-archive direct
curl -I "https://site.nl/author/admin/"
# Verwacht: 404 als je author-archives hebt uitgezet.
# 5. XML-RPC
curl -X POST -d '<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName></methodCall>' \
https://site.nl/xmlrpc.php
# Verwacht: 403 of "XML-RPC services are disabled".Voor een geautomatiseerde scan: de Invicti-vulnerability-referentie voor username enumeration legt de exacte checks uit die een pentester zou draaien, plus WPScan CLI en Sucuri’s online-scanner voor de snelle check.
WAF, brute-force en 2FA als vangnet
Zelfs met perfecte enumeration-protectie: je account-passwords blijven een risico. Combineer altijd met:
- 2FA op admin- en editor-accounts. WP 2FA, Two Factor of Wordfence Login Security zijn alle drie prima. Verplicht 2FA voor administrators.
- Rate-limiting op
/wp-login.phpvia Wordfence, Fail2Ban of Cloudflare rate-limiting-rules. - Wachtwoordbeleid: minimum 14 karakters, forceer wachtwoord-manager-generatie voor admins.
- WAF: Cloudflare (gratis tier is prima) of Wordfence. Vangt de meeste bekende brute-force-patterns automatisch.
Enumeration dichten zonder 2FA is een halve maatregel. Andersom, alleen 2FA zonder enumeration-fix, is beter dan niets, maar geeft botnets nog altijd een target-lijst.
Wanneer schakel je Lypii in
Volledige enumeration-hardening plus WAF-regels op een gemiddelde WooCommerce-site: 2 tot 4 uur. Aan €95 per uur is dat €190 tot €380. Inclusief de zes filters in een mu-plugin, author-slug-loskoppeling, login-error-fix, en een korte oplever-docs met de vijf curl-commando’s zodat jij zelf kunt hertesten.
Als onderdeel van een bredere WordPress security audit checklist: dan gecombineerd goedkoper per uur. En voor de accounts zelf raad ik altijd twee-factor authenticatie voor WordPress aan, ongeacht of je de enumeration-fix doorvoert.
Ik werk niet ’s nachts, wel binnen de week. Verbeteren boven herbouwen: je site hoeft niet opnieuw opgebouwd te worden. Zes filters, één mu-plugin, klaar.
Multi-stack opmerking: dit artikel is WordPress-specifiek omdat de lekken zitten in hoe WordPress standaard URL’s en REST-endpoints bouwt. Laravel-, Statamic- en custom-PHP-sites hebben andere enumeration-vectoren; die vragen om aparte analyse.



