VYPR
Low severity2.7NVD Advisory· Published Jun 5, 2026

Omni: Operator can traverse image-factory API paths via unsanitized `talos_version` in CreateSchematic

CVE-2026-45723

Description

Summary

managementServer.CreateSchematic (internal/backend/grpc/schematics.go) passes the caller-controlled TalosVersion field directly to imageFactoryClient.OverlaysVersions, which embeds it verbatim into a fmt.Sprintf("/version/%s/overlays/official", talosVersion) path template. url.URL.JoinPath resolves any ../ sequences in that path, allowing an authenticated Operator to rewrite the URL path and force Omni to issue HTTP GET requests to unintended paths on the configured image-factory server. Error body content from those unintended endpoints is returned to the caller.

Severity

  • Attack Vector: Network: exploited via the gRPC CreateSchematic API endpoint.
  • Attack Complexity: Low: once the attacker holds an Operator credential and has identified a media ID with an overlay, exploitation is a single API call.
  • Privileges Required: High: role.Operator is required, which has administrative capabilities on Omni.
  • User Interaction: None.
  • Scope: Unchanged: the traversal is constrained to the configured image-factory host; the attacker cannot redirect Omni to an arbitrary external server.
  • Confidentiality Impact: Low: error body content from unintended image-factory endpoints is reflected back to the operator, potentially leaking server-internal information.
  • Integrity Impact: None: only HTTP GET requests are issued; no write operations are performed.
  • Availability Impact: None.

Impact

  • Same-host path traversal: An authenticated Operator can force Omni to issue GET requests to arbitrary URL paths on the configured image-factory server, bypassing the intended versioned overlay API structure.
  • Error-body disclosure: HTTP error responses from unintended image-factory endpoints are reflected back to the operator, potentially leaking server-internal diagnostics or sensitive path content.
  • Internal network probing: In deployments using a private image-factory instance on an internal network, the attacker can probe endpoint existence and partial responses through error-text differences.
  • Depth control: By varying the number of ../ prefixes in talosVersion, the attacker can reach any path hierarchy on the image-factory host.

Credit

This vulnerability was discovered and reported by bugbunny.ai.

Affected products

2

Patches

2
9426c2cabcca

fix: add more input validations to management API

https://github.com/siderolabs/omniUtku OzdemirMay 11, 2026Fixed in 1.7.3via ghsa-release-walk
3 files changed · +35 4
  • internal/backend/grpc/management.go+6 2 modified
    @@ -677,8 +677,12 @@ func (s *managementServer) CreateJoinToken(ctx context.Context, request *managem
     
     	joinToken := siderolinkres.NewJoinToken(token)
     
    -	if request.Name == "" && len(request.Name) > omni.MaxJoinTokenNameLength {
    -		return nil, status.Error(codes.InvalidArgument, "token name is invalid")
    +	if request.Name == "" {
    +		return nil, status.Error(codes.InvalidArgument, "token name cannot be empty")
    +	}
    +
    +	if len(request.Name) > omni.MaxJoinTokenNameLength {
    +		return nil, status.Errorf(codes.InvalidArgument, "token name cannot be longer than %d symbols", omni.MaxJoinTokenNameLength)
     	}
     
     	joinToken.TypedSpec().Value.Name = request.Name
    
  • internal/backend/grpc/schematics.go+7 0 modified
    @@ -10,6 +10,7 @@ import (
     	"fmt"
     	"slices"
     
    +	"github.com/blang/semver/v4"
     	"github.com/cosi-project/runtime/pkg/safe"
     	"github.com/siderolabs/image-factory/pkg/client"
     	"github.com/siderolabs/image-factory/pkg/schematic"
    @@ -125,6 +126,12 @@ func (s *managementServer) CreateSchematic(ctx context.Context, request *managem
     }
     
     func (s *managementServer) getOverlay(ctx context.Context, req *management.CreateSchematicRequest) (schematic.Overlay, error) {
    +	if req.TalosVersion != "" {
    +		if _, err := semver.ParseTolerant(req.TalosVersion); err != nil {
    +			return schematic.Overlay{}, status.Error(codes.InvalidArgument, "invalid Talos version")
    +		}
    +	}
    +
     	if !quirks.New(req.TalosVersion).SupportsOverlay() {
     		return schematic.Overlay{}, nil
     	}
    
  • internal/backend/grpc/schematics_test.go+22 2 modified
    @@ -167,6 +167,11 @@ func (suite *GrpcSuite) TestSchematicCreate() {
     
     	suite.Require().NoError(suite.state.Create(ctx, media))
     
    +	overlayMedia := omni.NewInstallationMedia("overlay-test")
    +	overlayMedia.TypedSpec().Value.Overlay = "test-overlay"
    +
    +	suite.Require().NoError(suite.state.Create(ctx, overlayMedia))
    +
     	for _, tt := range []struct {
     		request       *management.CreateSchematicRequest
     		expectedError func(*testing.T, error)
    @@ -269,10 +274,25 @@ func (suite *GrpcSuite) TestSchematicCreate() {
     				require.Equal(t, codes.InvalidArgument, status.Code(err))
     			},
     		},
    +		{
    +			name: "invalid Talos version",
    +			request: &management.CreateSchematicRequest{
    +				TalosVersion: "../../secret",
    +				MediaId:      "overlay-test",
    +			},
    +			expectedError: func(t *testing.T, err error) {
    +				require.Equal(t, codes.InvalidArgument, status.Code(err))
    +			},
    +		},
     	} {
     		req := tt.request
    -		req.TalosVersion = "v1.6.5"
    -		req.MediaId = "test"
    +		if req.TalosVersion == "" {
    +			req.TalosVersion = "v1.6.5"
    +		}
    +
    +		if req.MediaId == "" {
    +			req.MediaId = "test"
    +		}
     
     		suite.T().Run(tt.name, func(t *testing.T) {
     			resp, err := client.CreateSchematic(ctx, req)
    
3e69e8080262

fix: add more input validations to management API

https://github.com/siderolabs/omniUtku OzdemirMay 11, 2026Fixed in 1.6.6via ghsa-release-walk
4 files changed · +87 4
  • internal/backend/grpc/management.go+6 2 modified
    @@ -676,8 +676,12 @@ func (s *managementServer) CreateJoinToken(ctx context.Context, request *managem
     
     	joinToken := siderolinkres.NewJoinToken(token)
     
    -	if request.Name == "" && len(request.Name) > omni.MaxJoinTokenNameLength {
    -		return nil, status.Error(codes.InvalidArgument, "token name is invalid")
    +	if request.Name == "" {
    +		return nil, status.Error(codes.InvalidArgument, "token name cannot be empty")
    +	}
    +
    +	if len(request.Name) > omni.MaxJoinTokenNameLength {
    +		return nil, status.Errorf(codes.InvalidArgument, "token name cannot be longer than %d symbols", omni.MaxJoinTokenNameLength)
     	}
     
     	joinToken.TypedSpec().Value.Name = request.Name
    
  • internal/backend/grpc/management_validation_test.go+52 0 added
    @@ -0,0 +1,52 @@
    +// Copyright (c) 2026 Sidero Labs, Inc.
    +//
    +// Use of this software is governed by the Business Source License
    +// included in the LICENSE file.
    +
    +package grpc_test
    +
    +import (
    +	"context"
    +	"strings"
    +	"testing"
    +
    +	"github.com/stretchr/testify/require"
    +	"go.uber.org/zap/zaptest"
    +	"google.golang.org/grpc/codes"
    +	"google.golang.org/grpc/status"
    +
    +	"github.com/siderolabs/omni/client/api/omni/management"
    +	grpcomni "github.com/siderolabs/omni/internal/backend/grpc"
    +	omniruntime "github.com/siderolabs/omni/internal/backend/runtime/omni"
    +	"github.com/siderolabs/omni/internal/pkg/auth"
    +	"github.com/siderolabs/omni/internal/pkg/auth/role"
    +	"github.com/siderolabs/omni/internal/pkg/ctxstore"
    +)
    +
    +func TestCreateJoinTokenValidation(t *testing.T) {
    +	server := grpcomni.NewManagementServer(nil, nil, zaptest.NewLogger(t), false, nil, nil)
    +	ctx := ctxstore.WithValue(context.Background(), auth.EnabledAuthContextKey{Enabled: true})
    +	ctx = ctxstore.WithValue(ctx, auth.RoleContextKey{Role: role.Admin})
    +
    +	for _, tt := range []struct {
    +		request *management.CreateJoinTokenRequest
    +		name    string
    +	}{
    +		{
    +			name:    "empty name",
    +			request: &management.CreateJoinTokenRequest{},
    +		},
    +		{
    +			name: "name too long",
    +			request: &management.CreateJoinTokenRequest{
    +				Name: strings.Repeat("x", omniruntime.MaxJoinTokenNameLength+1),
    +			},
    +		},
    +	} {
    +		t.Run(tt.name, func(t *testing.T) {
    +			_, err := server.CreateJoinToken(ctx, tt.request)
    +			require.Error(t, err)
    +			require.Equal(t, codes.InvalidArgument, status.Code(err))
    +		})
    +	}
    +}
    
  • internal/backend/grpc/schematics.go+7 0 modified
    @@ -10,6 +10,7 @@ import (
     	"fmt"
     	"slices"
     
    +	"github.com/blang/semver/v4"
     	"github.com/cosi-project/runtime/pkg/safe"
     	"github.com/siderolabs/image-factory/pkg/client"
     	"github.com/siderolabs/image-factory/pkg/schematic"
    @@ -125,6 +126,12 @@ func (s *managementServer) CreateSchematic(ctx context.Context, request *managem
     }
     
     func (s *managementServer) getOverlay(ctx context.Context, req *management.CreateSchematicRequest) (schematic.Overlay, error) {
    +	if req.TalosVersion != "" {
    +		if _, err := semver.ParseTolerant(req.TalosVersion); err != nil {
    +			return schematic.Overlay{}, status.Error(codes.InvalidArgument, "invalid Talos version")
    +		}
    +	}
    +
     	if !quirks.New(req.TalosVersion).SupportsOverlay() {
     		return schematic.Overlay{}, nil
     	}
    
  • internal/backend/grpc/schematics_test.go+22 2 modified
    @@ -167,6 +167,11 @@ func (suite *GrpcSuite) TestSchematicCreate() {
     
     	suite.Require().NoError(suite.state.Create(ctx, media))
     
    +	overlayMedia := omni.NewInstallationMedia("overlay-test")
    +	overlayMedia.TypedSpec().Value.Overlay = "test-overlay"
    +
    +	suite.Require().NoError(suite.state.Create(ctx, overlayMedia))
    +
     	for _, tt := range []struct {
     		request       *management.CreateSchematicRequest
     		expectedError func(*testing.T, error)
    @@ -269,10 +274,25 @@ func (suite *GrpcSuite) TestSchematicCreate() {
     				require.Equal(t, codes.InvalidArgument, status.Code(err))
     			},
     		},
    +		{
    +			name: "invalid Talos version",
    +			request: &management.CreateSchematicRequest{
    +				TalosVersion: "../../secret",
    +				MediaId:      "overlay-test",
    +			},
    +			expectedError: func(t *testing.T, err error) {
    +				require.Equal(t, codes.InvalidArgument, status.Code(err))
    +			},
    +		},
     	} {
     		req := tt.request
    -		req.TalosVersion = "v1.6.5"
    -		req.MediaId = "test"
    +		if req.TalosVersion == "" {
    +			req.TalosVersion = "v1.6.5"
    +		}
    +
    +		if req.MediaId == "" {
    +			req.MediaId = "test"
    +		}
     
     		suite.T().Run(tt.name, func(t *testing.T) {
     			resp, err := client.CreateSchematic(ctx, req)
    

Vulnerability mechanics

Root cause

"The `TalosVersion` field is not sufficiently validated before being embedded into a URL path, allowing path traversal."

Attack vector

An authenticated Operator with administrative capabilities on Omni can exploit this vulnerability by making a single gRPC API call to `CreateSchematic`. The attacker crafts a request with a specially malformed `TalosVersion` field containing `../` sequences. This allows them to rewrite the URL path that Omni uses to request overlays from the image-factory server.

Affected code

The vulnerability resides in the `managementServer.CreateSchematic` function within `internal/backend/grpc/schematics.go`. Specifically, the `TalosVersion` field from the incoming request is passed directly to `imageFactoryClient.OverlaysVersions` without proper sanitization. The patch modifies `internal/backend/grpc/schematics.go` to include semver validation for the `TalosVersion` field.

What the fix does

The patch introduces validation for the `TalosVersion` field using semantic versioning parsing before it is used in the image factory client call [patch_id=4935412]. This prevents path traversal by rejecting invalid version strings that could contain `../` sequences. The validation ensures that the `TalosVersion` adheres to a safe format, thereby mitigating the risk of accessing unintended paths on the image-factory server.

Preconditions

  • authThe attacker must possess `role.Operator` credentials.
  • inputThe attacker must identify a media ID with an overlay.

Generated on Jun 5, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.