VYPR
Medium severity4.4NVD Advisory· Published Jul 22, 2024· Updated Apr 15, 2026

CVE-2024-41129

CVE-2024-41129

Description

The ops library is a Python framework for developing and testing Kubernetes and machine charms. The issue here is that ops passes the secret content as one of the args via CLI. This issue may affect any of the charms that are using: Juju (>=3.0), Juju secrets and not correctly capturing and processing subprocess.CalledProcessError. This vulnerability is fixed in 2.15.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
opsPyPI
>= 2.0.0, < 2.15.02.15.0

Patches

1
fea6d2072435

fix: use temp dir for secret data (#1290)

https://github.com/canonical/operatorDima TisnekJul 22, 2024via ghsa
3 files changed · +40 18
  • ops/model.py+14 9 modified
    @@ -3598,11 +3598,13 @@ def secret_set(
                 args.extend(['--expire', expire.isoformat()])
             if rotate is not None:
                 args += ['--rotate', rotate.value]
    -        if content is not None:
    -            # The content has already been validated with Secret._validate_content
    -            for k, v in content.items():
    -                args.append(f'{k}={v}')
    -        self._run_for_secret('secret-set', *args)
    +        with tempfile.TemporaryDirectory() as tmp:
    +            # The content is None or has already been validated with Secret._validate_content
    +            for k, v in (content or {}).items():
    +                with open(f'{tmp}/{k}', mode='w', encoding='utf-8') as f:
    +                    f.write(v)
    +                args.append(f'{k}#file={tmp}/{k}')
    +            self._run_for_secret('secret-set', *args)
     
         def secret_add(
             self,
    @@ -3625,10 +3627,13 @@ def secret_add(
                 args += ['--rotate', rotate.value]
             if owner is not None:
                 args += ['--owner', owner]
    -        # The content has already been validated with Secret._validate_content
    -        for k, v in content.items():
    -            args.append(f'{k}={v}')
    -        result = self._run('secret-add', *args, return_output=True)
    +        with tempfile.TemporaryDirectory() as tmp:
    +            # The content has already been validated with Secret._validate_content
    +            for k, v in content.items():
    +                with open(f'{tmp}/{k}', mode='w', encoding='utf-8') as f:
    +                    f.write(v)
    +                args.append(f'{k}#file={tmp}/{k}')
    +            result = self._run('secret-add', *args, return_output=True)
             secret_id = typing.cast(str, result)
             return secret_id.strip()
     
    
  • test/test_helpers.py+12 0 modified
    @@ -153,6 +153,15 @@ def write(self, name: str, content: str):
                 f.write(
                     """#!/bin/sh
     {{ printf {name}; printf "\\036%s" "$@"; printf "\\034"; }} >> {path}/calls.txt
    +
    +# Capture key and data from key#file=/some/path arguments
    +for word in "$@"; do
    +  echo "$word" | grep -q "#file=" || continue
    +  key=$(echo "$word" | cut -d'#' -f1)
    +  path=$(echo "$word" | cut -d'=' -f2)
    +  cp "$path" "{path}/$key.secret"
    +done
    +
     {content}""".format_map(template_args)
                 )
             path.chmod(0o755)
    @@ -175,6 +184,9 @@ def calls(self, clear: bool = False) -> typing.List[typing.List[str]]:
                     f.truncate(0)
             return calls
     
    +    def secrets(self) -> typing.Dict[str, str]:
    +        return {p.stem: p.read_text() for p in self.path.iterdir() if p.suffix == '.secret'}
    +
     
     class FakeScriptTest(unittest.TestCase):
         def test_fake_script_works(self):
    
  • test/test_model.py+14 9 modified
    @@ -24,7 +24,7 @@
     import unittest
     from collections import OrderedDict
     from textwrap import dedent
    -from unittest.mock import MagicMock, patch
    +from unittest.mock import ANY, MagicMock, patch
     
     import pytest
     
    @@ -3349,7 +3349,8 @@ def test_app_add_secret_simple(self, fake_script: FakeScript, model: ops.Model):
             assert secret.id == 'secret:123'
             assert secret.label is None
     
    -        assert fake_script.calls(clear=True) == [['secret-add', '--owner', 'application', 'foo=x']]
    +        assert fake_script.calls(clear=True) == [['secret-add', '--owner', 'application', ANY]]
    +        assert fake_script.secrets() == {'foo': 'x'}
     
         def test_app_add_secret_args(self, fake_script: FakeScript, model: ops.Model):
             fake_script.write('secret-add', 'echo secret:234')
    @@ -3379,10 +3380,11 @@ def test_app_add_secret_args(self, fake_script: FakeScript, model: ops.Model):
                     'hourly',
                     '--owner',
                     'application',
    -                'foo=x',
    -                'bar=y',
    +                ANY,
    +                ANY,
                 ]
             ]
    +        assert fake_script.secrets() == {'foo': 'x', 'bar': 'y'}
     
         def test_unit_add_secret_simple(self, fake_script: FakeScript, model: ops.Model):
             fake_script.write('secret-add', 'echo secret:345')
    @@ -3392,7 +3394,8 @@ def test_unit_add_secret_simple(self, fake_script: FakeScript, model: ops.Model)
             assert secret.id == 'secret:345'
             assert secret.label is None
     
    -        assert fake_script.calls(clear=True) == [['secret-add', '--owner', 'unit', 'foo=x']]
    +        assert fake_script.calls(clear=True) == [['secret-add', '--owner', 'unit', ANY]]
    +        assert fake_script.secrets() == {'foo': 'x'}
     
         def test_unit_add_secret_args(self, fake_script: FakeScript, model: ops.Model):
             fake_script.write('secret-add', 'echo secret:456')
    @@ -3422,10 +3425,11 @@ def test_unit_add_secret_args(self, fake_script: FakeScript, model: ops.Model):
                     'yearly',
                     '--owner',
                     'unit',
    -                'foo=w',
    -                'bar=z',
    +                ANY,
    +                ANY,
                 ]
             ]
    +        assert fake_script.secrets() == {'foo': 'w', 'bar': 'z'}
     
         def test_unit_add_secret_errors(self, model: ops.Model):
             # Additional add_secret tests are done in TestApplication
    @@ -3721,10 +3725,11 @@ def test_set_content(self, model: ops.Model, fake_script: FakeScript):
                 secret.set_content({'s': 't'})  # ensure it validates content (key too short)
     
             assert fake_script.calls(clear=True) == [
    -            ['secret-set', 'secret:x', 'foo=bar'],
    +            ['secret-set', 'secret:x', ANY],
                 ['secret-info-get', '--label', 'y', '--format=json'],
    -            ['secret-set', 'secret:z', 'bar=foo'],
    +            ['secret-set', 'secret:z', ANY],
             ]
    +        assert fake_script.secrets() == {'foo': 'bar', 'bar': 'foo'}
     
         def test_set_info(self, model: ops.Model, fake_script: FakeScript):
             fake_script.write('secret-set', """exit 0""")
    

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

4

News mentions

0

No linked articles in our index yet.