Shared registry server enforces only its own home project's Permission policies, breaking per-project isolation for every other project it serves
- 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
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