Skip to content

Shared registry server enforces only its own home project's Permission policies, breaking per-project isolation for every other project it serves #6784

Description

@geo-chen

reported on 7 July 2026 via https://github.com/feast-dev/feast/security/advisories/GHSA-5px7-7gwg-6g93:

Summary

Feast's registry server (gRPC and REST) is designed to serve multiple projects from a single process - the documented REST API accepts a project query parameter on every request and even has /all endpoints that aggregate results across every project on the server. However, the RBAC SecurityManager that enforces Permission policies is initialized once at server startup using only the server's own configured project, and always loads that one project's permission list for every request it handles, regardless of which project the request actually targets. On a registry server that serves more than one project (an explicitly documented and supported topology), a user who is only granted a role in the server's home project gains that role's access to every other project's resources as well, and permissions defined specifically for those other projects are never consulted at all.

Details

SecurityManager is constructed once when the server starts, bound to the feature store's own project:

# sdk/python/feast/permissions/security_manager.py:24-33
class SecurityManager:
    def __init__(self, project: str, registry: BaseRegistry):
        self._project = project
        self._registry = registry
        ...

# sdk/python/feast/permissions/server/utils.py:66-84
def init_security_manager(auth_type: AuthManagerType, fs: "feast.FeatureStore"):
    ...
    set_security_manager(SecurityManager(project=fs.project, registry=fs.registry))

fs.project is whatever project: is set in the server process's own feature_store.yaml - a single, fixed value for the life of the process.

Every permission check goes through this same, single SecurityManager instance's permissions property:

# sdk/python/feast/permissions/security_manager.py:50-56
@property
def permissions(self) -> list[Permission]:
    return self._registry.list_permissions(project=self._project)

Note this always uses self._project (the server's home project), never anything derived from the current request. Compare this against how every other registry RPC handles the project field - e.g. ListEntities:

# sdk/python/feast/registry_server.py:304-319
def ListEntities(self, request: RegistryServer_pb2.ListEntitiesRequest, context):
    paginated_entities, pagination_metadata = apply_pagination_and_sorting(
        permitted_resources(
            resources=cast(list[FeastObject], self.proxied_registry.list_entities(
                project=request.project,   # <-- correctly scoped to the REQUESTED project
                ...
            )),
            actions=AuthzedAction.DESCRIBE,   # permitted_resources -> SecurityManager.permissions,
        ),                                     #   which is scoped to the SERVER's home project, not request.project
        ...
    )

The data fetch is correctly scoped per-request; the permission check that gates it is not. Permission.match_resource (sdk/python/feast/permissions/matcher.py) only matches on resource type, name pattern, and tags - it has no concept of the resource's own project either, so a broad permission (no name_patterns, matching ALL_RESOURCE_TYPES - a completely ordinary way to define a project's reader role) matches resources from any project once it is even considered.

Net effect on a registry server that serves N projects from one process:

  • A permission granted only in the server's home project silently applies to every other project's resources too (over-broad grant).
  • Permissions that were specifically defined for those other projects are never loaded or consulted at all (the intended, project-owner-defined policy is silently ignored).

PoC

Setup: one Feast REST Registry Server (feast serve_registry --no-grpc --rest-api), one shared SQLite registry, two projects each independently defined via feast apply:

  • project_a: entity user_id, feature view project_a_secret_metrics, Permission project_a_reader_permission = RoleBasedPolicy(roles=["project_a_reader"]), actions=[DESCRIBE]+READ.
  • project_b: entity user_id, feature view project_b_public_metrics, Permission project_b_reader_permission = RoleBasedPolicy(roles=["project_b_reader"]), actions=[DESCRIBE]+READ.

The registry server process itself was started from project_a's feature_store.yaml (i.e. its home project is project_a).

A genuine, JWKS-signature-verified token for user bob holding only project_a_reader (no project_b_reader):

$ curl -s -H "Authorization: Bearer $GOODA" "http://127.0.0.1:6572/api/v1/entities?project=project_a"
HTTP_STATUS:200   (expected: bob is a legitimate project_a reader)

The same token, same role, queried against project_b - bob holds no role granted by project_b's own permission (project_b_reader_permission requires project_b_reader), so this should be denied or return an empty/filtered list:

$ curl -s -H "Authorization: Bearer $GOODA" "http://127.0.0.1:6572/api/v1/entities?project=project_b"
{"entities":[{"spec":{"name":"__dummy", ...}},{"spec":{"name":"user_id","joinKey":"user_id"}, ...}]}
HTTP_STATUS:200

Direct fetch of project_b's feature view (including its data source path) with the same project_a-only token:

$ curl -s -H "Authorization: Bearer $GOODA" \
    "http://127.0.0.1:6572/api/v1/feature_views/project_b_public_metrics?project=project_b"
{"type":"featureView","spec":{"name":"project_b_public_metrics","entities":["user_id"],
"features":[{"name":"public_score","valueType":"FLOAT"}],"tags":{"owner":"project_b_team","sensitivity":"public"},
"batchSource":{"fileOptions":{"uri":"/.../data/project_b_data.parquet"}, ...}}, ...}
HTTP_STATUS:200

project_b_reader_permission (the policy project_b's own owner defined specifically to gate access to this data) was never consulted - bob's access was granted purely because project_a_reader_permission has no project scoping and matches any Entity/FeatureView regardless of which project it belongs to, combined with the server only ever loading project_a's permission list.

Impact

Any Feast deployment that runs one registry server (gRPC or REST) in front of more than one project - explicitly documented and UI-supported behavior - loses per-project access-control isolation between all of the projects that server serves. A user who is legitimately a low-privilege reader in one project inherits that same access to every other project's entities, feature views, data source definitions (including source file/table paths), and other registry metadata on the same server, while the actual owners of those other projects have no way to restrict it - their own permission definitions are simply never loaded.

version: commit f296d4b

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions