CVE-2022-39340 (Go)

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.

Weakness:
CWE-862: name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CWE-639: name: Authorization Bypass Through User-Controlled Key
description: The system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data.

Repository: https://github.com/openfga/openfga

Vulnerable code:
file_path: pkg/cmd/service/service.go
snippet: func BuildService(config *Config, logger logger.Logger) (*service, error) {
	tracer := telemetry.NewNoopTracer()
	meter := telemetry.NewNoopMeter()
	tokenEncoder := encoder.NewTokenEncoder(encrypter.NewNoopEncrypter(), encoder.NewBase64Encoder())

	var datastore storage.OpenFGADatastore
	var err error
	switch config.Datastore.Engine {
	case "memory":
		datastore = memory.New(tracer, config.MaxTuplesPerWrite, config.MaxTypesPerAuthorizationModel)
	case "mysql":
		opts := []mysql.MySQLOption{
			mysql.WithLogger(logger),
			mysql.WithTracer(tracer),
		}

		datastore, err = mysql.NewMySQLDatastore(config.Datastore.URI, opts...)
		if err != nil {
			return nil, errors.Errorf("failed to initialize mysql datastore: %v", err)
		}
	case "postgres":
		opts := []postgres.PostgresOption{
			postgres.WithLogger(logger),
			postgres.WithTracer(tracer),
		}

		datastore, err = postgres.NewPostgresDatastore(config.Datastore.URI, opts...)
		if err != nil {
			return nil, errors.Errorf("failed to initialize postgres datastore: %v", err)
		}
	default:
		return nil, errors.Errorf("storage engine '%s' is unsupported", config.Datastore.Engine)
	}

	logger.Info(fmt.Sprintf("using '%v' storage engine", config.Datastore.Engine))

	var grpcTLSConfig *server.TLSConfig
	if config.GRPC.TLS.Enabled {
		if config.GRPC.TLS.CertPath == "" || config.GRPC.TLS.KeyPath == "" {
			return nil, ErrInvalidGRPCTLSConfig
		}
		grpcTLSConfig = &server.TLSConfig{
			CertPath: config.GRPC.TLS.CertPath,
			KeyPath:  config.GRPC.TLS.KeyPath,
		}
		logger.Info("grpc TLS is enabled, serving connections using the provided certificate")
	} else {
		logger.Warn("grpc TLS is disabled, serving connections using insecure plaintext")
	}

	var httpTLSConfig *server.TLSConfig
	if config.HTTP.TLS.Enabled {
		if config.HTTP.TLS.CertPath == "" || config.HTTP.TLS.KeyPath == "" {
			return nil, ErrInvalidHTTPTLSConfig
		}
		httpTLSConfig = &server.TLSConfig{
			CertPath: config.HTTP.TLS.CertPath,
			KeyPath:  config.HTTP.TLS.KeyPath,
		}
		logger.Info("HTTP TLS is enabled, serving HTTP connections using the provided certificate")
	} else {
		logger.Warn("HTTP TLS is disabled, serving connections using insecure plaintext")
	}

	var authenticator authn.Authenticator
	switch config.Authn.Method {
	case "none":
		logger.Warn("authentication is disabled")
		authenticator = authn.NoopAuthenticator{}
	case "preshared":
		logger.Info("using 'preshared' authentication")
		authenticator, err = presharedkey.NewPresharedKeyAuthenticator(config.Authn.Keys)
	case "oidc":
		logger.Info("using 'oidc' authentication")
		authenticator, err = oidc.NewRemoteOidcAuthenticator(config.Authn.Issuer, config.Authn.Audience)
	default:
		return nil, errors.Errorf("unsupported authentication method '%v'", config.Authn.Method)
	}
	if err != nil {
		return nil, errors.Errorf("failed to initialize authenticator: %v", err)
	}

	interceptors := []grpc.UnaryServerInterceptor{
		grpc_auth.UnaryServerInterceptor(middleware.AuthFunc(authenticator)),
		middleware.NewErrorLoggingInterceptor(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")
	}

	if grpcHostAddr == "" {
		grpcHostAddr = "0.0.0.0"
	}

	grpcAddr, err := netip.ParseAddrPort(fmt.Sprintf("%s:%s", grpcHostAddr, grpcHostPort))
	if err != nil {
		return nil, errors.Errorf("failed to parse the 'grpc.addr' config: %v", err)
	}

	httpHostAddr, httpHostPort, err := net.SplitHostPort(config.HTTP.Addr)
	if err != nil {
		return nil, errors.Errorf("`http.addr` config must be in the form [host]:port")
	}

	if httpHostAddr == "" {
		httpHostAddr = "0.0.0.0"
	}

	httpAddr, err := netip.ParseAddrPort(fmt.Sprintf("%s:%s", httpHostAddr, httpHostPort))
	if err != nil {
		return nil, errors.Errorf("failed to parse the 'http.addr' config: %v", err)
	}

	openFgaServer, err := server.New(&server.Dependencies{
		Datastore:    caching.NewCachedOpenFGADatastore(datastore, config.Datastore.MaxCacheSize),
		Tracer:       tracer,
		Logger:       logger,
		Meter:        meter,
		TokenEncoder: tokenEncoder,
	}, &server.Config{
		GRPCServer: server.GRPCServerConfig{
			Addr:      grpcAddr,
			TLSConfig: grpcTLSConfig,
		},
		HTTPServer: server.HTTPServerConfig{
			Enabled:            config.HTTP.Enabled,
			Addr:               httpAddr,
			TLSConfig:          httpTLSConfig,
			UpstreamTimeout:    config.HTTP.UpstreamTimeout,
			CORSAllowedOrigins: config.HTTP.CORSAllowedOrigins,
			CORSAllowedHeaders: config.HTTP.CORSAllowedHeaders,
		},
		ResolveNodeLimit:       config.ResolveNodeLimit,
		ChangelogHorizonOffset: config.ChangelogHorizonOffset,
		ListObjectsDeadline:    config.ListObjectsDeadline,
		ListObjectsMaxResults:  config.ListObjectsMaxResults,
		UnaryInterceptors:      interceptors,
		MuxOptions:             nil,
	})
	if err != nil {
		return nil, errors.Errorf("failed to initialize openfga server: %v", err)
	}

	return &service{
		server:        openFgaServer,
		grpcAddr:      grpcAddr,
		httpAddr:      httpAddr,
		datastore:     datastore,
		authenticator: authenticator,
	}, nil
}

Fixed code:
file_path: pkg/cmd/service/service.go
snippet: func BuildService(config *Config, logger logger.Logger) (*service, error) {
	tracer := telemetry.NewNoopTracer()
	meter := telemetry.NewNoopMeter()
	tokenEncoder := encoder.NewTokenEncoder(encrypter.NewNoopEncrypter(), encoder.NewBase64Encoder())

	var datastore storage.OpenFGADatastore
	var err error
	switch config.Datastore.Engine {
	case "memory":
		datastore = memory.New(tracer, config.MaxTuplesPerWrite, config.MaxTypesPerAuthorizationModel)
	case "mysql":
		opts := []mysql.MySQLOption{
			mysql.WithLogger(logger),
			mysql.WithTracer(tracer),
		}

		datastore, err = mysql.NewMySQLDatastore(config.Datastore.URI, opts...)
		if err != nil {
			return nil, errors.Errorf("failed to initialize mysql datastore: %v", err)
		}
	case "postgres":
		opts := []postgres.PostgresOption{
			postgres.WithLogger(logger),
			postgres.WithTracer(tracer),
		}

		datastore, err = postgres.NewPostgresDatastore(config.Datastore.URI, opts...)
		if err != nil {
			return nil, errors.Errorf("failed to initialize postgres datastore: %v", err)
		}
	default:
		return nil, errors.Errorf("storage engine '%s' is unsupported", config.Datastore.Engine)
	}

	logger.Info(fmt.Sprintf("using '%v' storage engine", config.Datastore.Engine))

	var grpcTLSConfig *server.TLSConfig
	if config.GRPC.TLS.Enabled {
		if config.GRPC.TLS.CertPath == "" || config.GRPC.TLS.KeyPath == "" {
			return nil, ErrInvalidGRPCTLSConfig
		}
		grpcTLSConfig = &server.TLSConfig{
			CertPath: config.GRPC.TLS.CertPath,
			KeyPath:  config.GRPC.TLS.KeyPath,
		}
		logger.Info("grpc TLS is enabled, serving connections using the provided certificate")
	} else {
		logger.Warn("grpc TLS is disabled, serving connections using insecure plaintext")
	}

	var httpTLSConfig *server.TLSConfig
	if config.HTTP.TLS.Enabled {
		if config.HTTP.TLS.CertPath == "" || config.HTTP.TLS.KeyPath == "" {
			return nil, ErrInvalidHTTPTLSConfig
		}
		httpTLSConfig = &server.TLSConfig{
			CertPath: config.HTTP.TLS.CertPath,
			KeyPath:  config.HTTP.TLS.KeyPath,
		}
		logger.Info("HTTP TLS is enabled, serving HTTP connections using the provided certificate")
	} else {
		logger.Warn("HTTP TLS is disabled, serving connections using insecure plaintext")
	}

	var authenticator authn.Authenticator
	switch config.Authn.Method {
	case "none":
		logger.Warn("authentication is disabled")
		authenticator = authn.NoopAuthenticator{}
	case "preshared":
		logger.Info("using 'preshared' authentication")
		authenticator, err = presharedkey.NewPresharedKeyAuthenticator(config.Authn.Keys)
	case "oidc":
		logger.Info("using 'oidc' authentication")
		authenticator, err = oidc.NewRemoteOidcAuthenticator(config.Authn.Issuer, config.Authn.Audience)
	default:
		return nil, errors.Errorf("unsupported authentication method '%v'", config.Authn.Method)
	}
	if err != nil {
		return nil, errors.Errorf("failed to initialize authenticator: %v", err)
	}

	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")
	}

	if grpcHostAddr == "" {
		grpcHostAddr = "0.0.0.0"
	}

	grpcAddr, err := netip.ParseAddrPort(fmt.Sprintf("%s:%s", grpcHostAddr, grpcHostPort))
	if err != nil {
		return nil, errors.Errorf("failed to parse the 'grpc.addr' config: %v", err)
	}

	httpHostAddr, httpHostPort, err := net.SplitHostPort(config.HTTP.Addr)
	if err != nil {
		return nil, errors.Errorf("`http.addr` config must be in the form [host]:port")
	}

	if httpHostAddr == "" {
		httpHostAddr = "0.0.0.0"
	}

	httpAddr, err := netip.ParseAddrPort(fmt.Sprintf("%s:%s", httpHostAddr, httpHostPort))
	if err != nil {
		return nil, errors.Errorf("failed to parse the 'http.addr' config: %v", err)
	}

	openFgaServer, err := server.New(&server.Dependencies{
		Datastore:    caching.NewCachedOpenFGADatastore(datastore, config.Datastore.MaxCacheSize),
		Tracer:       tracer,
		Logger:       logger,
		Meter:        meter,
		TokenEncoder: tokenEncoder,
	}, &server.Config{
		GRPCServer: server.GRPCServerConfig{
			Addr:      grpcAddr,
			TLSConfig: grpcTLSConfig,
		},
		HTTPServer: server.HTTPServerConfig{
			Enabled:            config.HTTP.Enabled,
			Addr:               httpAddr,
			TLSConfig:          httpTLSConfig,
			UpstreamTimeout:    config.HTTP.UpstreamTimeout,
			CORSAllowedOrigins: config.HTTP.CORSAllowedOrigins,
			CORSAllowedHeaders: config.HTTP.CORSAllowedHeaders,
		},
		ResolveNodeLimit:       config.ResolveNodeLimit,
		ChangelogHorizonOffset: config.ChangelogHorizonOffset,
		ListObjectsDeadline:    config.ListObjectsDeadline,
		ListObjectsMaxResults:  config.ListObjectsMaxResults,
		UnaryInterceptors:      unaryServerInterceptors,
		StreamingInterceptors:  streamingServerInterceptors,
		MuxOptions:             nil,
	})
	if err != nil {
		return nil, errors.Errorf("failed to initialize openfga server: %v", err)
	}

	return &service{
		server:        openFgaServer,
		grpcAddr:      grpcAddr,
		httpAddr:      httpAddr,
		datastore:     datastore,
		authenticator: authenticator,
	}, nil
}