Tinklaraštis

wp-json: WordPress išduoda visų vartotojų prisijungimo vardus.

wp-json
Linux / Saugumas / Wordpress / Wordpress

wp-json: WordPress išduoda visų vartotojų prisijungimo vardus.

Prancūzijos nekilnojamojo turto agentūra paleido naują WordPress svetainę. Po kelių valandų ji buvo įsilaužta. Analizuodamas žurnalus, specialistas rado paprastą grandinę: pirma GET užklausa į /wp-json/wp/v2/users — prisijungimo vardai surinkti. Tada brutalios jėgos ataka per xmlrpc.php. Slaptažodis pasirodė esąs toks pat kaip prisijungimo vardas.

Svetainė neveikė kelias dienas. Savininkas net nežinojo, kad WordPress viešai išduoda visų vartotojų sąrašą — tai veikia pagal nutylėjimą, be jokių papildomų nustatymų.

Prie bet kurios WordPress svetainės adreso pridėk /wp-json/wp/v2/users ir atidaro naršyklėje. Jei niekas neuždaro — pamatysi maždaug tai:

[
  {
    "id": 1,
    "name": "admin",
    "slug": "admin",
    "link": "https://example.com/author/admin/",
    "url": "https://example.com"
  },
  {
    "id": 2,
    "name": "john.doe",
    "slug": "john-doe",
    "link": "https://example.com/author/john-doe/"
  }
]

Jokios autorizacijos. Paprastas curl — ir visų prisijungimo vardų sąrašas tavo rankose.

WordPress tai padarė specialiai. Ne klaida — funkcija. Todėl visi ir praleidžia.

KODĖL ŠIS ENDPOINT APSKRITAI EGZISTUOJA

WordPress REST API gavo infrastruktūrą 4.4 versijoje (2015 m. gruodis) — bazines klases ir maršrutizavimą. Tačiau content endpoint, įskaitant /wp/v2/users, atsirado tik 4.7 versijoje (2016 m. gruodis). Visas šiuolaikinis WordPress remiasi šiuo API: Gutenberg blokinė redaktorius, mobiliosios programėlės, headless CMS integracijos.

Todėl negalima paprasčiausiai išjungti REST API — sulaužys redaktorių, įrašai nebus išsaugomi. Tikslas ne užmušti API, o uždaryti konkretų endpoint kuris neturi reikalo būti viešas.

Tarp dešimčių maršrutų yra vienas: /wp-json/wp/v2/users. Jis grąžina visus vartotojus kurie publikavo įrašus. Be autorizacijos. Pagal dizainą — išorinių integracijų palaikymui. Bet įprastai svetainei tai nereikalinga informacija.

KĄ TIKSLIAI MATO UŽPUOLIKAS

curl -s https://example.com/wp-json/wp/v2/users | python3 -m json.tool

Kiekvienam vartotojui atsakyme yra: id — skaitmeninis duomenų bazės identifikatorius, name — rodomas vardas, slug — raidinių-skaitmeninių simbolių identifikatorius naudojamas autoriaus URL, link — tiesioginė nuoroda į autoriaus puslapį.

Čia svarbus niuansas kurį daugelis painioja. Laukas username — tikrasis prisijungimo vardas — oficialios WordPress dokumentacijos turi context edit. Tai reiškia, kad anonimiams jis nematomas. Matomas tik autentifikuotiems vartotojams su redaktoriaus teisėmis.

Bet laukas slug turi context view — atviras visiems. Ir čia yra problema: WordPress pagal nutylėjimą generuoja slug iš username registracijos metu. Jei administratorius slug nekeitė — slug ir yra prisijungimo vardas. Užpuolikas jį gauna tiesiogiai.

Slaptažodžiai ir el. pašto adresai nenuteka. Bet vartotojo identifikatorius užpuoliko rankose — tai jau pusė darbo brutalios jėgos atakai.

Ir dar daugiau. Pagrindinis endpoint /wp-json/ be jokių teisių grąžina pilną viso API žemėlapį:

# Kurie papildiniai užregistravo savo endpoint
curl -s https://example.com/wp-json/ | grep -o '"namespace":"[^"]*"' | sort -u

Iš užregistruotų namespace nuskaitomas visas stack: WooCommerce, Yoast SEO, Contact Form 7. Užpuolikas palygina su CVE duomenų baze ir pasirenka vektorių.

KODĖL TAI PAVOJINGA

Štai reali grandinė iš pentest ataskaitų:

GET /wp-json/wp/v2/users       → surinkti prisijungimo vardus
GET /wp-json/                   → nustatyti įdiegtus papildinius
→ palyginti su CVE duomenų baze
→ brutalios jėgos ataka su žinomais vardais arba tikslingas išnaudojimas

Visas ciklas — mažiau nei penkios minutės su automatizuotais įrankiais.

Atskira istorija apie apsaugos papildinius. CVE-2025-4302 parodė: „Stop User Enumeration” iki 1.7.3 versijos blokavo /wp-json/wp/v2/users — bet ne /wp-json/wp%2Fv2%2Fusers. Paprastas slashų URL kodavimas apeidavo visą apsaugą. Papildinys su 50 000 diegimų neapsaugojo. Teisingas sprendimas — WordPress kodo lygmeniu arba nginx, ne papildinys.

KAIP UŽDARYTI

1 būdas: WordPress filtras (rekomenduojama)

Trys eilutės mu-plugin faile. Gutenberg toliau veikia — jis naudoja autentifikuotas užklausas. Blokuojama tik anoniminė prieiga:

cat > /var/www/html/wp-content/mu-plugins/restrict-rest-users.php << 'EOF'
<?php
/**
 * Plugin Name: Restrict REST API Users Endpoint
 * Description: Blocks unauthenticated access to /wp-json/wp/v2/users
 */
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;
});
EOF

Patikrinti:

curl -s https://example.com/wp-json/wp/v2/users
# Laukiama: {"code":"rest_no_route","message":"No route was found...","data":{"status":404}}

Šis būdas uždaro ir /wp-json/wp/v2/users ir alternatyvų kelią /?rest_route=/wp/v2/users — abu apdorojami tuo pačiu PHP lygmens filtru.

2 būdas: nginx

Jei reikia blokavimo prieš PHP — greičiau ir be serverio apkrovos:

location ~* ^/wp-json/wp/v2/users {
    deny all;
    access_log off;
    log_not_found off;
}

# Alternatyvus kelias per ?rest_route=
# if veikia tik server{} kontekste, ne location{} bloko viduje
# Dėti prieš location blokus:
if ($arg_rest_route ~* "^/wp/v2/users") {
    return 403;
}
nginx -t && systemctl reload nginx
curl -I https://example.com/wp-json/wp/v2/users
# Laukiama: HTTP/1.1 403 Forbidden

Pastaba: nginx blokavimas uždaro endpoint visiškai — įskaitant autentifikuotas užklausas. Daugumai tinklaraščių tai normalu. Jei naudoji papildinius kurie dirba su vartotojais per REST API — saugesnis 1 būdas.

Uždaryti pagrindinį endpoint anonimiams

/wp-json/ išduoda papildinių žemėlapį be autorizacijos. Uždaryti:

add_filter('rest_authentication_errors', function($result) {
    if (!is_user_logged_in() && isset($_SERVER['REQUEST_URI']) &&
        rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') === '/wp-json') {
        return new WP_Error('rest_forbidden', 'REST API root restricted.', ['status' => 403]);
    }
    return $result;
});

Arba nginx:

location = /wp-json/ {
    deny all;
    access_log off;
    log_not_found off;
}

Pašalinti REST API discovery nuorodą iš HTML

WordPress kiekvienam puslapiui prideda:

<link rel='https://api.w.org/' href='https://example.com/wp-json/' />

Botai tai skaito kaip signalą, kad REST API aktyvus. Pašalinti:

// Pridėti į remove-legacy-links.php
remove_action('wp_head', 'rest_output_link_wp_head', 10);

ANTRAS VEKTORIUS: ?author=1

REST API nėra vienintelis kelias. WordPress pagal nutylėjimą peradresuoja ?author=1 į /author/username/, ir vartotojo slug atsiduria tiesiai URL. Botai nuosekliai perrenkia skaičius:

curl -I "https://example.com/?author=1"
# HTTP/1.1 301 Moved Permanently
# Location: https://example.com/author/admin/  ← prisijungimo vardas atskleistas

Uždaryti per nginx:

location / {
    if ($args ~* "^author=\d+$") {
        return 403;
    }
}

Arba per WordPress:

add_action('template_redirect', function() {
    if (is_author() && isset($_GET['author'])) {
        wp_redirect(home_url(), 301);
        exit;
    }
});

DAŽNOS KLAIDOS

„Neturiu viešų autorių — endpoint tuščias.” Dabar tuščias. Pridėsi autorių su įrašu — duomenys atsiras. Geriau uždaryti iš karto.

„Pakeitiau rodomą vardą — prisijungimo vardas nematomas.” name ir slug yra skirtingi laukai. Rodomą vardą galima keisti kiek nori. Slug autoriaus URL ir endpoint lieka.

„Uždariau endpoint — Gutenberg sugedo.” Filtras rest_endpoints su is_user_logged_in() neliečia autentifikuotų užklausų. Jei sugedo — patikrink ar blokas taikomas tik anonimiams.

„Įdiegiau Stop User Enumeration.” CVE-2025-4302 — apeinamas URL koduojant kelią. Ne sprendimas.

PATIKRINK SAVO SVETAINĘ

# Endpoint atviras anonimiams?
curl -s -o /dev/null -w "%{http_code}" https://example.com/wp-json/wp/v2/users
# 200 = atviras, 403/404 = uždarytas

# Atskiras vartotojas pagal ID
curl -s -o /dev/null -w "%{http_code}" https://example.com/wp-json/wp/v2/users/1

# Alternatyvus kelias uždarytas?
curl -s -o /dev/null -w "%{http_code}" "https://example.com/?rest_route=/wp/v2/users"

# Ar veikia author enumeration?
curl -sI "https://example.com/?author=1" | grep Location

# Kokie namespace atskleisti — kurie papildiniai matomi?
curl -s https://example.com/wp-json/ | grep -o '"namespace":"[^"]*"' | sort -u

# REST API nuoroda HTML?
curl -s https://example.com/ | grep "api.w.org"

IŠVADA

/wp-json/wp/v2/users pagal nutylėjimą atviras daugelyje WordPress svetainių. Ne todėl, kad kūrėjai suklydo — taip sukurta išorinių integracijų palaikymui. Bet įprastai svetainei tai nereikalinga: užpuolikas gauna prisijungimo vardus, papildinių namespace ir paruoštą atakos žemėlapį.

Pastaba: WordPress 6.9.1 (2026 m. vasaris) įvedė papildomą REST API apsaugą — tyrinėtojų duomenimis, kartu su saugumo papildiniais tai sumažina sėkmingo surašymo tikimybę maždaug 80%. Tačiau endpoint vis dar grąžina duomenis pagal dizainą — apsauga riboja piktnaudžiavimą, bet neuždaro prieigos. Filtras mu-plugin faile reikalingas bet kuriuo atveju.

Filtras mu-plugin faile uždaro problemą be šalutinių poveikių. Viršuje — nginx blokavimui prieš PHP. Ir pašalinti discovery nuorodą iš HTML kad nepatektum į automatinius skanus.

Patikrink dabar pat. Endpoint greičiausiai atviras.

Leave your thought here

El. pašto adresas nebus skelbiamas. Būtini laukeliai pažymėti *