
Vaultwarden 1.37.3: доверенный локальный X-Real-IP по умолчанию обходит ограничения частоты входов в систему при Docker NAT
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · vaultwarden-ip-header-spoof
Vaultwarden 1.37.3 - dani-garcia
Значения по умолчанию IP_HEADER=X-Real-IP и IP_HEADER_TRUSTED_PROXIES=local заставляют считать любой неглобальный TCP-пир обратным прокси. Штатный NAT опубликованного порта Docker относится к RFC1918. Предоставленный клиентом X-Real-IP становится ключом ограничения скорости. Уникальные значения никогда не делят один всплеск.
| ID | CVE пока нет |
| CWE | CWE-307 / CWE-290 |
| CVSS | High: 7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
| Продукт | Vaultwarden |
| Затронуто | 1.37.3 (eb212e23). Образ vaultwarden/server:1.37.3. Docker published-port / userland-proxy / Desktop NAT или любой пир RFC1918. |
| Аутентификация | не требуется |
| Лицензия | GNU Affero GPL v3.0 |
| Лаборатория | только 127.0.0.1 |
Атакующий устраняет защиту от брутфорса, привязанную к IP. Он отправляет новый X-Real-IP при каждой попытке. Ограничитель — это DashMap с ключом по этому адресу, поэтому всплеск и период никогда не срабатывают.
POST /identity/connect/token. По умолчанию всплеск — 10 за 60 секунд на IP. Уникальные заголовки сбрасывают счётчик./api/two-factor/send-email-login, POST /admin, а также unauth hint / prelogin / delete-recover / auth-request / Send access — все используют ключ ClientIp.docker run -p 80:80 и compose 80:80 делают пиром адрес bridge или Desktop NAT. trusted=local — это !is_global(peer). Это совпадает.Обратный прокси, который заменяет X-Real-IP на реального клиента, и привязка на хосте, чей пир является глобальным адресом, сохраняют честность ограничителя. Дыра — это значение по умолчанию плюс опубликованный порт без перезаписи.
Я зафиксировал dani-garcia/vaultwarden 1.37.3 (eb212e23) после GHSA для 1.35.x / 1.36.0 / 1.37.0. GHSA-c5rv поместил send-email-login под LIMITER_LOGIN. Остаётся вопрос, как выбирается ClientIp.
config.rs по умолчанию задаёт ip_header как X-Real-IP, а ip_header_trusted_proxies как local. auth.rs ip_header_is_trusted трактует local как !is_global(remote). ratelimit.rs использует этот IpAddr как ключ для LIMITER_LOGIN. Docker bridge, Desktop VM NAT и loopback — все неглобальные.
Я поднял штатный vaultwarden/server:1.37.3 на loopback 127.0.0.1:18170:80 без overwrite-proxy, а также второй экземпляр на 18171 с IP_HEADER=none. Зарегистрировал [email protected]. Password-grant с неверным паролем и без заголовка получил 429 на запросе 11 (всплеск 10). Тот же grant с X-Real-IP: 198.51.100.{n}, уникальным при каждой попытке, оставался HTTP 400 четырнадцать раз, ноль 429. На экземпляре с none уникальные 203.0.113.{n} всё равно получили 429 на 11.
SUCCESS VAULTWARDEN-IP-HEADER-SPOOF nohdr-429-at=11 spoof-400=14 spoof-429=0 none-429-at=11 VAULTWARDEN-IP-HEADER-SPOOF-WITNESS
Уже зафиксированные неверные шаги: образ 1.37.3 не был локальным, поэтому первый compose ждал загрузки; первый /alive был пустым ответом, пока Rocket привязывался, затем 200 на второй попытке ожидания; фиктивного JSON регистрации (masterPasswordHash + короткий key) было достаточно, без повторной попытки с длинным ключом. Обратная оболочка. Театр. Оракул — это 429 без заголовка и 400 с уникальным.
cd lab
./run.sh
Цель — только http://127.0.0.1:18170 (по умолчанию) и http://127.0.0.1:18171 (IP_HEADER=none). Образ vaultwarden/server:1.37.3.
Задать IP_HEADER по умолчанию как none или задать IP_HEADER_TRUSTED_PROXIES по умолчанию как пустой список / явный CIDR прокси. local в значении «любой пир RFC1918» — это случай опубликованного порта. Наличие заголовка пересылки не является доказательством наличия прокси.
src/config.rs (ip_header, ip_header_trusted_proxies)src/auth.rs (ip_header_is_trusted, ClientIp)src/ratelimit.rs