T3MP3ST public launch build
This commit is contained in:
1 parent
972247b881
commit
184c7d4e59
1589 files changed
+57150
-506
No files matched your search
@@ -0,0 +1,124 @@
|
||||
--- .changelog/27919.txt
|
||||
@@ -0,0 +1,3 @@
|
||||
+```release-note:security
|
||||
+dynamic host volumes: Prevent unintended code execution outside the plugin directory (CVE-2026-7474)
|
||||
+```
|
||||
|
||||
--- client/hostvolumemanager/host_volume_plugin.go
|
||||
@@ -223,20 +223,27 @@ var _ HostVolumePlugin = &HostVolumePluginExternal{}
|
||||
// if the specified executable exists on disk.
|
||||
func NewHostVolumePluginExternal(log hclog.Logger,
|
||||
pluginDir, filename, volumesDir, nodePool string) (*HostVolumePluginExternal, error) {
|
||||
- // this should only be called with already-detected executables,
|
||||
- // but we'll double-check it anyway, so we can provide a tidy error message
|
||||
- // if it has changed between fingerprinting and execution.
|
||||
- executable := filepath.Join(pluginDir, filename)
|
||||
- f, err := os.Stat(executable)
|
||||
+ // this should only be called with already-detected executables, but we'll
|
||||
+ // double-check it anyway, so we can provide a tidy error message if it has
|
||||
+ // changed between fingerprinting and execution and to ensure that the
|
||||
+ // plugin ID can't traverse outside the plugin directory.
|
||||
+ root, err := os.OpenRoot(pluginDir)
|
||||
if err != nil {
|
||||
- if os.IsNotExist(err) {
|
||||
- return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, filename)
|
||||
- }
|
||||
- return nil, err
|
||||
+ return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, filename)
|
||||
+ }
|
||||
+
|
||||
+ f, err := root.Stat(filename)
|
||||
+ if err != nil {
|
||||
+ // note we intentionally obscure the root-escape error here and return a
|
||||
+ // ErrPluginNotExists, because there's no legitimate reason to ever get
|
||||
+ // this error
|
||||
+ return nil, fmt.Errorf("%w: %q", ErrPluginNotExists, filename)
|
||||
}
|
||||
if !helper.IsExecutable(f) {
|
||||
return nil, fmt.Errorf("%w: %q", ErrPluginNotExecutable, filename)
|
||||
}
|
||||
+ executable := filepath.Join(pluginDir, filename)
|
||||
+
|
||||
return &HostVolumePluginExternal{
|
||||
ID: filename,
|
||||
Executable: executable,
|
||||
|
||||
--- client/hostvolumemanager/host_volume_plugin_test.go
|
||||
@@ -218,6 +218,14 @@ func TestNewHostVolumePluginExternal(t *testing.T) {
|
||||
_, err = NewHostVolumePluginExternal(log, ".", "non-existent", "target", "")
|
||||
must.ErrorIs(t, err, ErrPluginNotExists)
|
||||
|
||||
+ // OpenRoot fails
|
||||
+ _, err = NewHostVolumePluginExternal(log, "/xxxxxxxxxxxxxxxxx", "non-existent", "target", "")
|
||||
+ must.ErrorIs(t, err, ErrPluginNotExists)
|
||||
+
|
||||
+ // tries to escape root
|
||||
+ _, err = NewHostVolumePluginExternal(log, ".", "../non-existent", "target", "")
|
||||
+ must.ErrorIs(t, err, ErrPluginNotExists)
|
||||
+
|
||||
_, err = NewHostVolumePluginExternal(log, ".", "host_volume_plugin_test.go", "target", "")
|
||||
must.ErrorIs(t, err, ErrPluginNotExecutable)
|
||||
|
||||
|
||||
--- client/hostvolumemanager/host_volumes_test.go
|
||||
@@ -363,7 +363,7 @@ func TestHostVolumeManager_restoreFromState(t *testing.T) {
|
||||
}
|
||||
must.NoError(t, state.PutDynamicHostVolume(vol))
|
||||
|
||||
- hvm := NewHostVolumeManager(log, Config{StateMgr: state})
|
||||
+ hvm := NewHostVolumeManager(log, Config{StateMgr: state, PluginDir: t.TempDir()})
|
||||
vols, err := hvm.restoreFromState(timeout(t))
|
||||
must.ErrorIs(t, err, ErrPluginNotExists)
|
||||
must.MapEmpty(t, vols)
|
||||
|
||||
--- nomad/host_volume_endpoint.go
|
||||
@@ -521,6 +521,18 @@ func (v *HostVolume) registerVolume(vol *structs.HostVolume) error {
|
||||
// by that name. It returns the node (for testing) and an error indicating
|
||||
// placement failed.
|
||||
func (v *HostVolume) placeHostVolume(snap *state.StateSnapshot, vol *structs.HostVolume) (*structs.Node, error) {
|
||||
+ ctx := &placementContext{
|
||||
+ regexpCache: make(map[string]*regexp.Regexp),
|
||||
+ versionCache: make(map[string]feasible.VerConstraints),
|
||||
+ semverCache: make(map[string]feasible.VerConstraints),
|
||||
+ }
|
||||
+ constraints := []*structs.Constraint{{
|
||||
+ LTarget: fmt.Sprintf("${attr.plugins.host_volume.%s.version}", vol.PluginID),
|
||||
+ Operand: "is_set",
|
||||
+ }}
|
||||
+ constraints = append(constraints, vol.Constraints...)
|
||||
+ checker := feasible.NewConstraintChecker(ctx, constraints)
|
||||
+
|
||||
if vol.NodeID != "" {
|
||||
node, err := snap.NodeByID(nil, vol.NodeID)
|
||||
if err != nil {
|
||||
@@ -529,6 +541,10 @@ func (v *HostVolume) placeHostVolume(snap *state.StateSnapshot, vol *structs.Hos
|
||||
if node == nil {
|
||||
return nil, fmt.Errorf("no such node %s", vol.NodeID)
|
||||
}
|
||||
+ if ok := checker.Feasible(node); !ok {
|
||||
+ return nil, fmt.Errorf("node %s is not feasible for volume", vol.NodeID)
|
||||
+ }
|
||||
+
|
||||
vol.NodePool = node.NodePool
|
||||
return node, nil
|
||||
}
|
||||
@@ -552,19 +568,6 @@ func (v *HostVolume) placeHostVolume(snap *state.StateSnapshot, vol *structs.Hos
|
||||
return nil, err
|
||||
}
|
||||
|
||||
- var checker *feasible.ConstraintChecker
|
||||
- ctx := &placementContext{
|
||||
- regexpCache: make(map[string]*regexp.Regexp),
|
||||
- versionCache: make(map[string]feasible.VerConstraints),
|
||||
- semverCache: make(map[string]feasible.VerConstraints),
|
||||
- }
|
||||
- constraints := []*structs.Constraint{{
|
||||
- LTarget: fmt.Sprintf("${attr.plugins.host_volume.%s.version}", vol.PluginID),
|
||||
- Operand: "is_set",
|
||||
- }}
|
||||
- constraints = append(constraints, vol.Constraints...)
|
||||
- checker = feasible.NewConstraintChecker(ctx, constraints)
|
||||
-
|
||||
var (
|
||||
filteredByExisting int
|
||||
filteredByGovernance int
|
||||
Reference in new issue
Block a user