Moderate severityNVD Advisory· Published Oct 25, 2022· Updated Apr 23, 2025
OpenFGA Information Disclosure
CVE-2022-39340
Description
OpenFGA is an authorization/permission engine. Prior to version 0.2.4, the streamed-list-objects endpoint was not validating the authorization header, resulting in disclosure of objects in the store. Users openfga/openfga versions 0.2.3 and prior who are exposing the OpenFGA service to the internet are vulnerable. Version 0.2.4 contains a patch for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | < 0.2.4 | 0.2.4 |
Affected products
1Patches
1779d73d4b6d0Merge pull request from GHSA-95x7-mh78-7w2r
4 files changed · +154 −70
pkg/cmd/service/service.go+8 −2 modified@@ -359,11 +359,16 @@ func BuildService(config *Config, logger logger.Logger) (*service, error) { return nil, errors.Errorf("failed to initialize authenticator: %v", err) } - interceptors := []grpc.UnaryServerInterceptor{ + unaryServerInterceptors := []grpc.UnaryServerInterceptor{ grpc_auth.UnaryServerInterceptor(middleware.AuthFunc(authenticator)), middleware.NewErrorLoggingInterceptor(logger), } + streamingServerInterceptors := []grpc.StreamServerInterceptor{ + grpc_auth.StreamServerInterceptor(middleware.AuthFunc(authenticator)), + middleware.NewStreamingErrorLoggingInterceptor(logger), + } + grpcHostAddr, grpcHostPort, err := net.SplitHostPort(config.GRPC.Addr) if err != nil { return nil, errors.Errorf("`grpc.addr` config must be in the form [host]:port") @@ -415,7 +420,8 @@ func BuildService(config *Config, logger logger.Logger) (*service, error) { ChangelogHorizonOffset: config.ChangelogHorizonOffset, ListObjectsDeadline: config.ListObjectsDeadline, ListObjectsMaxResults: config.ListObjectsMaxResults, - UnaryInterceptors: interceptors, + UnaryInterceptors: unaryServerInterceptors, + StreamingInterceptors: streamingServerInterceptors, MuxOptions: nil, }) if err != nil {
pkg/cmd/service/service_test.go+121 −65 modified@@ -7,6 +7,7 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/json" "encoding/pem" "fmt" "io" @@ -24,6 +25,7 @@ import ( "github.com/hashicorp/go-retryablehttp" "github.com/openfga/openfga/pkg/logger" "github.com/openfga/openfga/server/authn/mocks" + serverErrors "github.com/openfga/openfga/server/errors" "github.com/stretchr/testify/require" "github.com/tidwall/gjson" openfgapb "go.buf.build/openfga/go/openfga/api/openfga/v1" @@ -33,6 +35,7 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" healthv1pb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/protobuf/encoding/protojson" ) func TestMain(m *testing.M) { @@ -205,9 +208,10 @@ func createCertsAndKeys(t *testing.T) certHandle { } type authTest struct { - _name string - authHeader string - expectedError string + _name string + authHeader string + expectedErrorResponse *serverErrors.ErrorResponse + expectedStatusCode int } func TestBuildServiceWithNoAuth(t *testing.T) { @@ -251,48 +255,39 @@ func TestBuildServiceWithPresharedKeyAuthentication(t *testing.T) { ensureServiceUp(t, service, nil, true) tests := []authTest{{ - _name: "Header with incorrect key fails", - authHeader: "Bearer incorrectkey", - expectedError: "unauthenticated", + _name: "Header with incorrect key fails", + authHeader: "Bearer incorrectkey", + expectedErrorResponse: &serverErrors.ErrorResponse{ + Code: "unauthenticated", + Message: "unauthenticated", + }, + expectedStatusCode: 401, }, { - _name: "Missing header fails", - authHeader: "", - expectedError: "missing bearer token", + _name: "Missing header fails", + authHeader: "", + expectedErrorResponse: &serverErrors.ErrorResponse{ + Code: "bearer_token_missing", + Message: "missing bearer token", + }, + expectedStatusCode: 401, }, { - _name: "Correct key one succeeds", - authHeader: "Bearer KEYONE", - expectedError: "", + _name: "Correct key one succeeds", + authHeader: fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[0]), + expectedStatusCode: 200, }, { - _name: "Correct key two succeeds", - authHeader: "Bearer KEYTWO", - expectedError: "", + _name: "Correct key two succeeds", + authHeader: fmt.Sprintf("Bearer %s", config.Authn.AuthnPresharedKeyConfig.Keys[1]), + expectedStatusCode: 200, }} retryClient := retryablehttp.NewClient() for _, test := range tests { t.Run(test._name, func(t *testing.T) { - payload := strings.NewReader(`{"name": "some-store-name"}`) - req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload) - require.NoError(t, err, "Failed to construct request") - req.Header.Set("content-type", "application/json") - req.Header.Set("authorization", test.authHeader) - - res, err := retryClient.Do(req) - require.NoError(t, err, "Failed to execute request") - - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - require.NoError(t, err, "Failed to read response") - - stringBody := string(body) - - if test.expectedError == "" && strings.Contains(stringBody, "code") { - t.Fatalf("Expected no error but got '%v'", stringBody) - } + tryGetStores(t, test, service, retryClient) + }) - if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" { - t.Fatalf("Expected '%v' to contain '%v'", stringBody, test.expectedError) - } + t.Run(test._name+"/streaming", func(t *testing.T) { + tryStreamingListObjects(t, test, service, retryClient, config.Authn.AuthnPresharedKeyConfig.Keys[0]) }) } @@ -301,6 +296,75 @@ func TestBuildServiceWithPresharedKeyAuthentication(t *testing.T) { require.NoError(t, g.Wait()) } +func tryStreamingListObjects(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client, validToken string) { + // create a store + createStorePayload := strings.NewReader(`{"name": "some-store-name"}`) + req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), createStorePayload) + require.NoError(t, err, "Failed to construct create store request") + req.Header.Set("content-type", "application/json") + req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken)) + res, err := retryClient.Do(req) + require.NoError(t, err, "Failed to execute create store request") + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err, "Failed to read create store response") + var createStoreResponse openfgapb.CreateStoreResponse + err = protojson.Unmarshal(body, &createStoreResponse) + require.NoError(t, err, "Failed to unmarshal create store response") + + // create an authorization model + authModelPayload := strings.NewReader(`{"type_definitions":[{"type":"document","relations":{"owner":{"this":{}}}}]}`) + req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/authorization-models", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), authModelPayload) + require.NoError(t, err, "Failed to construct create authorization model request") + req.Header.Set("content-type", "application/json") + req.Header.Set("authorization", fmt.Sprintf("Bearer %s", validToken)) + _, err = retryClient.Do(req) + require.NoError(t, err, "Failed to execute create authorization model request") + + // call one streaming endpoint + listObjectsPayload := strings.NewReader(`{"type": "document", "user": "anne", "relation": "owner"}`) + req, err = retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores/%s/streamed-list-objects", service.GetHTTPAddrPort().Port(), createStoreResponse.Id), listObjectsPayload) + require.NoError(t, err, "Failed to construct request") + req.Header.Set("content-type", "application/json") + req.Header.Set("authorization", test.authHeader) + + res, err = retryClient.Do(req) + require.Equal(t, test.expectedStatusCode, res.StatusCode) + require.NoError(t, err, "Failed to execute streaming request") + + defer res.Body.Close() + body, err = io.ReadAll(res.Body) + require.NoError(t, err, "Failed to read response") + + if test.expectedErrorResponse != nil { + require.Contains(t, string(body), fmt.Sprintf(",\"message\":\"%s\"", test.expectedErrorResponse.Message)) + } +} + +func tryGetStores(t *testing.T, test authTest, service *service, retryClient *retryablehttp.Client) { + req, err := retryablehttp.NewRequest("GET", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), nil) + require.NoError(t, err, "Failed to construct request") + req.Header.Set("content-type", "application/json") + req.Header.Set("authorization", test.authHeader) + + res, err := retryClient.Do(req) + require.NoError(t, err, "Failed to execute request") + require.Equal(t, test.expectedStatusCode, res.StatusCode) + + defer res.Body.Close() + body, err := io.ReadAll(res.Body) + require.NoError(t, err, "Failed to read response") + + if test.expectedErrorResponse != nil { + var actualErrorResponse serverErrors.ErrorResponse + err = json.Unmarshal(body, &actualErrorResponse) + + require.NoError(t, err, "Failed to unmarshal response") + + require.Equal(t, test.expectedErrorResponse, &actualErrorResponse) + } +} + func TestHTTPServerWithCORS(t *testing.T) { config, err := DefaultConfigWithRandomPorts() require.NoError(t, err) @@ -436,43 +500,35 @@ func TestBuildServerWithOIDCAuthentication(t *testing.T) { require.NoError(t, err) tests := []authTest{{ - _name: "Header with invalid token fails", - authHeader: "Bearer incorrecttoken", - expectedError: "invalid bearer token", + _name: "Header with invalid token fails", + authHeader: "Bearer incorrecttoken", + expectedErrorResponse: &serverErrors.ErrorResponse{ + Code: "auth_failed_invalid_bearer_token", + Message: "invalid bearer token", + }, + expectedStatusCode: 401, }, { - _name: "Missing header fails", - authHeader: "", - expectedError: "missing bearer token", + _name: "Missing header fails", + authHeader: "", + expectedErrorResponse: &serverErrors.ErrorResponse{ + Code: "bearer_token_missing", + Message: "missing bearer token", + }, + expectedStatusCode: 401, }, { - _name: "Correct token succeeds", - authHeader: "Bearer " + trustedToken, - expectedError: "", + _name: "Correct token succeeds", + authHeader: "Bearer " + trustedToken, + expectedStatusCode: 200, }} retryClient := retryablehttp.NewClient() for _, test := range tests { t.Run(test._name, func(t *testing.T) { - payload := strings.NewReader(`{"name": "some-store-name"}`) - req, err := retryablehttp.NewRequest("POST", fmt.Sprintf("http://localhost:%d/stores", service.GetHTTPAddrPort().Port()), payload) - require.NoError(t, err, "Failed to construct request") - req.Header.Set("content-type", "application/json") - req.Header.Set("authorization", test.authHeader) - - res, err := retryClient.Do(req) - require.NoError(t, err, "Failed to execute request") - - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - require.NoError(t, err, "Failed to read response") - - stringBody := string(body) - if test.expectedError == "" && strings.Contains(stringBody, "code") { - t.Fatalf("Expected no error but got %v", stringBody) - } + tryGetStores(t, test, service, retryClient) + }) - if !strings.Contains(stringBody, test.expectedError) && test.expectedError != "" { - t.Fatalf("Expected %v to contain %v", stringBody, test.expectedError) - } + t.Run(test._name+"/streaming", func(t *testing.T) { + tryStreamingListObjects(t, test, service, retryClient, trustedToken) }) }
server/middleware/logging.go+15 −0 modified@@ -25,3 +25,18 @@ func NewErrorLoggingInterceptor(logger logger.Logger) grpc.UnaryServerIntercepto return resp, nil } } + +func NewStreamingErrorLoggingInterceptor(logger logger.Logger) grpc.StreamServerInterceptor { + return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + err := handler(srv, stream) + if err != nil { + var e error + if internalError, ok := err.(serverErrors.InternalError); ok { + e = internalError.Internal() + } + logger.Error("grpc_error", zap.Error(e), zap.String("public_error", err.Error())) + } + + return err + } +}
server/server.go+10 −3 modified@@ -81,6 +81,7 @@ type Config struct { ListObjectsDeadline time.Duration ListObjectsMaxResults uint32 UnaryInterceptors []grpc.UnaryServerInterceptor + StreamingInterceptors []grpc.StreamServerInterceptor MuxOptions []runtime.ServeMuxOption } @@ -486,13 +487,19 @@ func (s *Server) IsReady(ctx context.Context) (bool, error) { // server cancel the provided ctx. func (s *Server) Run(ctx context.Context) error { - interceptors := []grpc.UnaryServerInterceptor{ + unaryServerInterceptors := []grpc.UnaryServerInterceptor{ grpc_validator.UnaryServerInterceptor(), } - interceptors = append(interceptors, s.config.UnaryInterceptors...) + unaryServerInterceptors = append(unaryServerInterceptors, s.config.UnaryInterceptors...) + + streamingInterceptors := []grpc.StreamServerInterceptor{ + grpc_validator.StreamServerInterceptor(), + } + streamingInterceptors = append(streamingInterceptors, s.config.StreamingInterceptors...) opts := []grpc.ServerOption{ - grpc.ChainUnaryInterceptor(interceptors...), + grpc.ChainUnaryInterceptor(unaryServerInterceptors...), + grpc.ChainStreamInterceptor(streamingInterceptors...), } if s.config.GRPCServer.TLSConfig != nil {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-95x7-mh78-7w2rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-39340ghsaADVISORY
- github.com/openfga/openfga/commit/779d73d4b6d067ee042ec9b59fec707eed71e42fghsaWEB
- github.com/openfga/openfga/releases/tag/v0.2.4ghsaWEB
- github.com/openfga/openfga/security/advisories/GHSA-95x7-mh78-7w2rghsaWEB
News mentions
0No linked articles in our index yet.