Før et angrep kommer rekognosering. Bots skanner etter lavthengende frukt: kjent WordPress-versjon, åpen xmlrpc.php, svake innloggingssider. Når HTML-en din sier «WordPress 6.4.2», matcher skanneren CVE-er mot den versjonen og går videre til exploit-fasen.
Å skjule versjonen stopper ikke en målrettet angriper. Det reduserer støy fra masse-skanning. Den reelle forsvarslinjen er HTTP-sikkerhetshodere, lukkede endepunkter og strenge filrettigheter. Denne guiden dekker begge lagene: først obscurity, deretter hardenings som faktisk blokkerer angrep i nettleseren.
Hvor lekker WordPress versjonsnummeret
Security through obscurity betyr å skjule systemdetaljer slik at opportunistiske angripere bruker mer tid eller hopper over deg. Kritikere har rett i at et system bør være sikkert selv om alt er kjent. I praksis er begge ting sanne: skjul det du kan, herde resten.
WordPress lekker versjon flere steder:
- meta-taggen
generatori HTML-head - RSS- og Atom-feeds
- query-strengen
?ver=på CSS og JS readme.htmloglicense.txti rotkatalogen
Offentlige sikkerhetspatcher lister berørte versjoner. Når boten ser 6.5.0, vet den hvilke kjente hull som fortsatt kan gjelde. Fjern den informasjonen, så faller du ut av de enkleste skriptene.
Fjerne meta generator og ?ver= i WordPress
Installer ikke en tung sikkerhetsplugin bare for dette. Legg kode i child-temaets functions.php eller en liten must-use plugin:
remove_action( 'wp_head', 'wp_generator' );
remove_action( 'wp_head', 'rsd_link' );
// Én filterlinje dekker generator-taggen i alle feedtyper
add_filter( 'the_generator', '__return_empty_string' );
// Fjern ?ver= bare når verdien er lik kjerneversjonen
function wppoland_strip_core_ver( string $src ): string {
global $wp_version;
$query = parse_url( $src, PHP_URL_QUERY );
if ( ! $query ) {
return $src;
}
parse_str( $query, $args );
if ( isset( $args['ver'] ) && $args['ver'] === $wp_version ) {
return remove_query_arg( 'ver', $src );
}
return $src;
}
add_filter( 'style_loader_src', 'wppoland_strip_core_ver', 9999 );
add_filter( 'script_loader_src', 'wppoland_strip_core_ver', 9999 );Cache-busting: Mange snutter fjerner ?ver= fra alle filer. Da beholder en plugin samme URL etter oppdatering, og nettlesere og CDN serverer den gamle filen. Sammenligningen med $wp_version skjuler kjerneversjonen og lar plugin- og temaversjoner være i fred.
Eldre utgaver av slike snutter fjernet også wlwmanifest_link. Den funksjonen er foreldet siden WordPress 6.3.0, fordi Windows Live Writer-manifestet ikke lenger følger med kjernen, så linjen gjør ingenting på en oppdatert installasjon.
Blokkere readme.html og license.txt
<FilesMatch "^(readme\.html|license\.txt)$">
Require all denied
</FilesMatch>Nginx:
location ~* /(readme\.html|license\.txt)$ {
deny all;
}Hvilke HTTP-headere avslører WordPress i tillegg til generator-taggen
Generator-taggen er ikke det eneste fingeravtrykket. Kjernen og PHP legger selv til responsheadere som forteller en skanner hvilken programvare nettstedet kjører, selv når ingen versjonsnummer står i HTML-en.
| Header | Sendes av | Når |
|---|---|---|
Link: <https://ditt-domene.no/wp-json/>; rel="https://api.w.org/" | rest_output_link_header(), siden WordPress 4.4.0 | på hver frontend-forespørsel, på template_redirect (prioritet 11) |
Link: <https://ditt-domene.no/?p=123>; rel=shortlink | wp_shortlink_header() | på enkeltinnlegg og sider, på template_redirect (prioritet 11) |
X-Pingback: https://ditt-domene.no/xmlrpc.php | WP::send_headers() | på enkeltinnlegg som tar imot pinger (pings_open()), betinget siden 4.4.0 |
X-Powered-By: PHP/8.3.x | PHP selv | så lenge expose_php er på, som er standard |
Kilder: referansen for rest_output_link_header, referansen for WP::send_headers og PHP-dokumentasjonen for kjernedirektivene.
WordPress-headerne fjernes med de samme hookene som legger dem til. X-Pingback går gjennom filteret wp_headers, som filtrerer HTTP-headerne før de sendes til nettleseren. Legg dette i samme mu-plugin:
// REST API-oppdagelse: Link-header og <link>-tagg
remove_action( 'template_redirect', 'rest_output_link_header', 11 );
remove_action( 'wp_head', 'rest_output_link_wp_head', 10 );
// Shortlink: header og <link>-tagg
remove_action( 'template_redirect', 'wp_shortlink_header', 11 );
remove_action( 'wp_head', 'wp_shortlink_wp_head', 10 );
// X-Pingback på enkeltinnlegg
add_filter( 'wp_headers', function ( array $headers ): array {
unset( $headers['X-Pingback'] );
return $headers;
} );Å fjerne oppdagelseslenken slår ikke av REST API-et: /wp-json/ svarer fortsatt, og blokkredigereren er avhengig av det. Målet er å slutte å annonsere CMS-et i hver HTML-respons, ikke å ødelegge API-et.
X-Powered-By kan ikke endres fra WordPress. PHP-manualen oppgir expose_php som en innstilling som bare kan settes i php.ini, så ini_set() har ingen effekt. Sett den i PHP-FPM-poolen eller i hoved-php.ini:
expose_php = OffOWASP HTTP Headers Cheat Sheet anbefaler å fjerne alle X-Powered-By-headere og enten fjerne Server eller gi den en verdi uten informasjon. Dokumentet minner også om at en angriper kan identifisere teknologistakken på andre måter. Det er den ærlige grensen for hele denne delen.
Sette sikkerhetsheadere i Apache og Nginx
Versjonsfjerning er å ta husnummeret av døra. Security headers er dødboltene. I 2026 forventer nettlesere disse headerne. Uten dem står du åpen for klikk-kapring, MIME-sniffing og mange XSS-varianter.
X-Frame-Options
Hindrer at siden lastes i en fremmed <iframe> (klikk-kapring).
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>add_header X-Frame-Options "SAMEORIGIN" always;SAMEORIGIN lar deg bygge inn egen side. DENY er strengere hvis du aldri trenger iframes.
X-Content-Type-Options
Header always set X-Content-Type-Options "nosniff"Sier til nettleseren: stol på Content-Type fra serveren. En opplastet «.jpg» med JS inne skal ikke kjøres som skript.
Strict-Transport-Security (HSTS)
Tvinger HTTPS etter første sikre besøk. Aktiver bare når sertifikatet er stabilt.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"max-age=31536000 = ett år. includeSubDomains dekker underdomener. preload er valgfritt og krever at du faktisk er klar for preload-listen (se egen del nedenfor). Feil HSTS kan låse besøkende ute i hele max-age-perioden, så test HTTPS grundig først.
Content-Security-Policy (CSP)
CSP begrenser hvilke kilder som kan laste skript, stiler og bilder. Det er den kraftigste headeren, og den som oftest ødelegger et WordPress-site hvis den settes for strengt uten testing.
Start i report-only:
Header set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.google-analytics.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; report-uri /csp-report"Når rapportene er rene, bytt til Content-Security-Policy (uten Report-Only). WordPress og mange plugins trenger fortsatt 'unsafe-inline' for skript og stiler, så planlegg en gradvis stramming i stedet for et brått skifte.
Referrer-Policy og Permissions-Policy
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"Referrer-Policy begrenser hva som lekker i Referer-headeren ved eksterne klikk. Permissions-Policy skrur av API-er du ikke bruker.
X-XSS-Protection i 2026
Eldre guider anbefalte X-XSS-Protection: 1; mode=block. Det rådet er utdatert. MDN-siden om X-XSS-Protection merker headeren som foreldet og ikke-standard, anbefaler Content-Security-Policy i stedet og advarer om at filteret i noen tilfeller kan skape XSS-sårbarheter på nettsteder som ellers er trygge. MDN legger til at mode=block kan være utsatt for sidekanalangrep når siden kan bygges inn i en <iframe>.
OWASP gir samme svar: ikke send headeren, eller sett den eksplisitt til 0. Sender en gammel serverkonfig eller en plugin allerede 1; mode=block, overskriver du verdien:
Header always set X-XSS-Protection "0"add_header X-XSS-Protection "0" always;Beskyttelsen du faktisk vil ha, kommer fra en CSP uten 'unsafe-inline' for skript. På de fleste WordPress-nettsteder er det et lengre prosjekt.
Sender WordPress X-Frame-Options selv
Ja, på to skjermer. Kjernen kobler send_frame_options_header() til login_init og, i wp-admin/includes/admin-filters.php, til admin_init. Funksjonen, tilgjengelig siden WordPress 3.1.3, sender to headere så lenge ingen headere er sendt ennå:
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self';Kilde: referansen for send_frame_options_header.
wp-login.php og dashbordet er altså beskyttet mot innramming også uten serverkonfig. Den offentlige frontenden er det ikke: temaer, kassesider og kontaktskjemaer får ikke denne headeren med mindre du legger den til.
Det har en praktisk konsekvens i Apache. Dokumentasjonen for mod_headers sier at set erstatter en eksisterende header med samme navn, mens append legger den nye verdien til den eksisterende, skilt med komma. Med Header append får innloggingssiden verdien fra WordPress pluss din egen. Med Header always set får den nøyaktig én. Samme dokumentasjon forklarer at headere som PHP lager bak mod_proxy_fcgi, ligger i always-tabellen. Derfor bruker eksemplene her Header always set. MDN påpeker i tillegg at direktivet ALLOW-FROM er foreldet og får moderne nettlesere til å ignorere hele headeren, og at X-Frame-Options satt i en <meta>-tagg ikke har noen effekt.
Hvorfor sikkerhetsheadere i Nginx forsvinner i location-blokker
En vanlig overraskelse etter en konfig som “virker”: curl -I mot forsiden viser alle sikkerhetsheaderne, men en CSS-fil eller wp-login.php viser ingen. Dokumentasjonen for ngx_http_headers_module forklarer hvorfor: add_header-direktiver arves fra forrige konfigurasjonsnivå hvis og bare hvis det ikke finnes noen add_header på gjeldende nivå.
Én add_header Cache-Control ... i location ~* \.(css|js)$ er nok til at den blokken mister alle sikkerhetsheaderne. To løsninger:
- Legg headerne i en snippet og inkluder den i
server-blokken og i hverlocationsom har egneadd_header-linjer:
# /etc/nginx/snippets/security-headers.conf
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;location ~* \.(css|js|woff2)$ {
include snippets/security-headers.conf;
add_header Cache-Control "public, max-age=31536000, immutable";
}- Fra Nginx 1.29.3 legger
add_header_inherit merge;verdiene fra forrige nivå til gjeldende nivå i stedet for å erstatte dem.
Parameteren always teller også. Uten den legger Nginx bare til headeren på svar med statuskode 200, 201, 204, 206, 301, 302, 303, 304, 307 eller 308. En 403 på /xmlrpc.php eller en 404-side går ut uten headere.
Hva HSTS preload-listen krever
preload i HSTS-eksemplet over virker ikke av seg selv. Det er en søknad om å bli hardkodet i nettleserne, og hstspreload.org lister vilkårene for innsending:
- lever et gyldig sertifikat,
- omdiriger fra HTTP til HTTPS på samme vert, hvis du lytter på port 80,
- lever alle underdomener over HTTPS, også
wwwhvis det finnes en DNS-post for det, - send HSTS-headeren på hoveddomenet over HTTPS med
max-agepå minst 31536000 sekunder,includeSubDomainsogpreload, - hvis HTTPS-siden omdirigerer videre, må selve omdirigeringen ha HSTS-headeren.
Samme side advarer om at preload gjelder alle underdomener, også interne som ikke er offentlig tilgjengelige, at nye oppføringer kan bruke flere måneder på å nå stabil Chrome, og at fjerning fra listen pleier å være treg og smertefull. Et staging-miljø på et underdomene uten sertifikat slutter å laste i alle nettlesere som har listen.
OWASP anbefaler max-age=63072000; includeSubDomains; preload, altså to år. Rekkefølgen som unngår utestengning: start med kort max-age uten preload, sjekk alle underdomener, øk verdien, og send inn domenet først når hele domenet er permanent kun HTTPS. Er du ikke klar for den forpliktelsen, fjerner du preload fra headeren.
Deaktivere xmlrpc.php i WordPress
xmlrpc.php brukes fortsatt av Jetpack, noen mobilapper og remote publishing. Hvis du ikke bruker noen av dem, er endepunktet en klassisk brute-force-vektor (spesielt multicall).
Apache:
<Files xmlrpc.php>
Require all denied
</Files>Nginx:
location = /xmlrpc.php {
deny all;
}PHP-filteret under er ikke en full blokkering:
add_filter( 'xmlrpc_enabled', '__return_false' );Referansen for xmlrpc_enabled sier at filteret, stikk i strid med navnet, bare styrer XML-RPC-metoder som krever autentisering. Pingbacks og andre endepunkter uten innlogging er fortsatt åpne. Serverblokken er sterkere, fordi forespørselen aldri når PHP.
Sikre WordPress-databasen
Endre tabellprefikset
Som standard bruker WordPress wp_ som tabellprefiks. Et eget prefiks hindrer ikke SQL injection; det får bare automatiserte skript som har wp_-tabellnavn hardkodet, til å feile.
Under installasjonen: sett et eget prefiks, for eksempel xyz_ eller prod_.
Etter installasjonen: bruk et plugin som Change Table Prefix, eller gi tabellene og de prefiksede nøklene i options og usermeta nye navn for hånd (ta sikkerhetskopi først).
Filrettighetene til wp-config.php og DISALLOW_FILE_EDIT står i neste seksjon.
Riktige filrettigheter for wp-config.php
Typiske verdier på delt hosting:
- Mapper:
755 - Filer:
644 wp-config.php:400eller440(kun lesbar for eier/gruppe)
Ikke sett 777 «fordi opplasting ikke fungerer». Fiks eierskap og PHP-brukeren i stedet. På managed host (Kinsta, WP Engine, Flywheel) følger du leverandørens modell; chmod-rådene over gjelder mest klassisk VPS og cPanel.
Slå av fileditoren i dashbordet
WordPress lar administratorer redigere tema- og pluginfiler fra dashbordet. Får en angriper en adminkonto, er det en direkte vei til å legge inn skadelig kode. Slå editoren av i wp-config.php:
define( 'DISALLOW_FILE_EDIT', true );Konstanten fjerner tema- og plugineditoren fra adminmenyen.
Begrense innloggingsforsøk og aktivere 2FA
Versjonsskjul hjelper lite hvis wp-login.php står åpen for passordgjetting. Praktisk minimum:
- Begrens innloggingsforsøk (plugin eller WAF-regel).
- Aktiver 2FA for alle administratorer.
- Unngå generiske brukernavn som
admin. - Overvåk mislykkede forsøk i serverlogger eller security-plugin.
Å endre innloggings-URL er obscurity igjen: nyttig mot støy, ikke erstatning for 2FA.
Aktuelle 2FA-plugins i den offisielle katalogen er Two Factor, publisert av WordPress.org-bidragsytere, og WP 2FA fra Melapress.
Stoppe brukeroppregning via REST API og ?author=
Som standard viser endepunktet /wp-json/wp/v2/users forfattere med slug, som ofte er lik brukernavnet, og /?author=1 omdirigerer til forfatterarkivet. Da mangler angriperen bare passordet. Skjul endepunktene for besøkende uten rettigheten list_users, og send forfatterforespørsler til forsiden:
add_filter( 'rest_endpoints', function ( array $endpoints ): array {
if ( current_user_can( 'list_users' ) ) {
return $endpoints;
}
unset( $endpoints['/wp/v2/users'], $endpoints['/wp/v2/users/(?P<id>[\d]+)'] );
return $endpoints;
} );
add_action( 'template_redirect', function (): void {
if ( is_author() && ! is_admin() ) {
wp_safe_redirect( home_url(), 301 );
exit;
}
} );Sjekken på list_users er viktig: uten den mister redaktører forfattervalget i blokkredigereren, som leser det samme endepunktet.
Teste WordPress-herding etter deploy
Etter deploy:
- Vis kildekode og sjekk at
generatorer borte. - Inspiser Network: CSS/JS fra kjernen skal ikke ha
?ver=6.x(med mindre du bevisst beholder egen cache-buster). - Bruk securityheaders.com eller
curl -I https://ditt-domene.no/for å lese responsheaders. - Bekreft at
/xmlrpc.phpreturnerer 403/404. - Bekreft at
/readme.htmler blokkert. - Bekreft at fingeravtrykkene er borte og at innloggingssiden har nøyaktig én
X-Frame-Options:
curl -sI https://ditt-domene.no/ | grep -iE "x-powered-by|x-pingback|api.w.org"
curl -sI https://ditt-domene.no/wp-login.php | grep -i x-frameKjør header-sjekken mot en URL som gir 404 også. I Nginx uten always er det der headerne mangler. I Chrome DevTools finner du det samme i Network-fanen under Response Headers for dokumentforespørselen.
WordPress-oppdateringer og sikkerhetsrevisjon
Oppdateringer av kjerne, tema og plugins er fortsatt viktigere enn obscurity. En uoppdatert plugin med RCE vinner over alle headers. Backup, staging før store oppgraderinger, og minst mulig angrepsflate (færre plugins) er del av samme jobb.
For en strukturert gjennomgang av sårbarheter og oppdateringsgjeld: se WordPress-sikkerhetsrevisjon. Trenger du hjelp til hardenings på et produksjonsmiljø: kontakt.
Skal sikkerhetskoden ligge i child-tema eller mu-plugin
Ikke lim hardeningskode direkte inn i et parent-tema du oppdaterer fra repo. Bruk child-temaets functions.php, eller bedre: en must-use plugin under wp-content/mu-plugins/. MU-plugins lastes alltid og overlever temabytte.
<?php
/**
* Plugin Name: WPPoland Hardening
* Description: Versjonsstripping og relaterte filtre. Headers settes i serverkonfig.
*/
require __DIR__ . '/wppoland-hardening/strip-version.php';Hold HTTP-headers i Nginx/Apache eller i hostens WAF (Cloudflare Transform Rules, for eksempel). PHP header() i send_headers fungerer, men forsvinner lettere ved caching-lag som stripper eller overskriver responsheaders.
Sikkerhetsheadere på Cloudflare og managed hosting
På Cloudflare kan du sette mange av de samme headerne som Transform Rules uten å røre .htaccess. På Kinsta, WP Engine og lignende er xmlrpc.php ofte allerede begrenset, så sjekk panelet før du dobbeltblokkerer. Fjern fortsatt generator i PHP; hosten gjør sjelden det for deg.
Når du bruker page cache (Nginx FastCGI, LiteSpeed, Cloudflare APO), verifiser headers på den cachede HTML-responsen, ikke bare på en innlogget forespørsel. En header som bare settes i PHP bak cache-miss hjelper ikke anonyme besøkende.
CSP-konflikter med WordPress-plugins
Google Analytics, GTM, cookie-banner, reCAPTCHA, Stripe og kart-embeds krever egne script-src / frame-src / connect-src-oppføringer. Start bredt i report-only, samle brudd i en uke, deretter stram. Unngå 'unsafe-eval' med mindre et konkret plugin krever det og du har akseptert risikoen.
Inline-skript i temaer (klassiske onclick og <script> i footer) tvinger ofte 'unsafe-inline'. Migrer til wp_enqueue_script med filer du hasher, så kan du senere bruke nonces eller hashes i CSP.
Feilsøke 403 og 500 etter WordPress-herding
Etter at du har blokkert XML-RPC og strammet innlogging, forvent støy i access-loggene (403 på /xmlrpc.php). Det er normalt. Det som ikke er normalt: plutselige 500-feil på forsiden etter CSP, eller innloggede redaktører som mister admin-AJAX fordi admin-ajax.php ble fanget av en for streng WAF-regel. Test wp-admin, Gutenberg-lagring og WooCommerce checkout hvis det finnes på samme domene.
Oppsummering
- Fjern generator, feed-versjon, kjerneversjonen i
?ver=og fingeravtrykk-headerne. - Sett X-Frame-Options, nosniff, HSTS, CSP (start report-only) og Referrer-Policy. Fjern X-XSS-Protection eller sett den til 0.
- Blokker
xmlrpc.phphvis ubrukt. - Stram filrettigheter, spesielt
wp-config.php. - Rate-limit + 2FA på innlogging.
- Verifiser med curl og securityheaders-sjekk etter deploy.
Skjul versjonen for å falle ut av billige skannere. Sett headers for å stoppe angrep nettleseren faktisk kan blokkere.







