feast-dev / feast-dev/feast

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

Open
#6,784 0 comments 0 reactions 0 assignees View on GitHub
kind/bug priority/p2
Dominant language
Python
Stars
7.3k
Forks
1.4k
Avg merge
1d 21h
Merged PRs (30d)
15

Description

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:

```python
# 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:

```python
# 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`:

```python
# 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 f296d4ba14c5d512429219b2b7845673e0fe524d

Contributor guide

Open the contributing guide

Research direction

Start with sdk/python/feast/permissions/security_manager.py and permissions/server/utils.py, then trace the project field through registry_server.py and matcher.py. Reproduce the REST requests from the PoC for project_a and project_b. Done means each request consults the target project's policies for both REST and gRPC, so a project_a-only token cannot access project_b resources.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.