VYPR
High severityNVD Advisory· Published Jul 8, 2025· Updated Jul 8, 2025

Arbitrary executable upload via authenticated endpoint

CVE-2025-0928

Description

In Juju versions prior to 3.6.8 and 2.9.52, any authenticated controller user was allowed to upload arbitrary agent binaries to any model or to the controller itself, without verifying model membership or requiring explicit permissions. This enabled the distribution of poisoned binaries to new or upgraded machines, potentially resulting in remote code execution.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/juju/jujuGo
< 0.0.0-20250619215741-4034aa13c7cf0.0.0-20250619215741-4034aa13c7cf

Affected products

1

Patches

4
22cdcf6b54c2

fix: ensure users uploading tools need admin permission on the model

https://github.com/juju/jujuwallyworldJun 18, 2025via ghsa
2 files changed · +35 13
  • apiserver/apiserver.go+12 11 modified
    @@ -708,11 +708,12 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     	healthHandler := srv.monitoredHandler(http.HandlerFunc(srv.healthHandler), "health")
     	logStreamHandler := srv.monitoredHandler(newLogStreamEndpointHandler(httpCtxt), "logstream")
     	embeddedCLIHandler := srv.monitoredHandler(newEmbeddedCLIHandler(httpCtxt), "commands")
    +	controllerAdminAuthorizer := controllerAdminAuthorizer{
    +		controllerTag: systemState.ControllerTag(),
    +	}
     	var debuglogAuth httpcontext.CompositeAuthorizer = []authentication.Authorizer{
     		tagKindAuthorizer{names.MachineTagKind, names.ControllerAgentTagKind},
    -		controllerAdminAuthorizer{
    -			controllerTag: systemState.ControllerTag(),
    -		},
    +		controllerAdminAuthorizer,
     		modelPermissionAuthorizer{
     			perm: permission.ReadAccess,
     		},
    @@ -757,9 +758,7 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		GetHandler:  modelCharmsHandler.ServeGet,
     	}, "charms")
     	var modelCharmsUploadAuthorizer httpcontext.CompositeAuthorizer = []authentication.Authorizer{
    -		controllerAdminAuthorizer{
    -			controllerTag: systemState.ControllerTag(),
    -		},
    +		controllerAdminAuthorizer,
     		modelPermissionAuthorizer{
     			perm: permission.WriteAccess,
     		},
    @@ -779,7 +778,12 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		ctxt:          httpCtxt,
     		stateAuthFunc: httpCtxt.stateForRequestAuthenticatedUser,
     	}, "tools")
    -	modelToolsUploadAuthorizer := tagKindAuthorizer{names.UserTagKind}
    +	var modelToolsUploadAuthorizer httpcontext.CompositeAuthorizer = []authentication.Authorizer{
    +		controllerAdminAuthorizer,
    +		modelPermissionAuthorizer{
    +			perm: permission.AdminAccess,
    +		},
    +	}
     	modelToolsDownloadHandler := srv.monitoredHandler(newToolsDownloadHandler(httpCtxt), "tools")
     	resourcesHandler := srv.monitoredHandler(&ResourcesHandler{
     		StateAuthFunc: func(req *http.Request, tagKinds ...string) (ResourcesBackend, state.PoolHelper, names.Tag,
    @@ -826,9 +830,6 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		},
     	}, "units")
     
    -	controllerAdminAuthorizer := controllerAdminAuthorizer{
    -		controllerTag: systemState.ControllerTag(),
    -	}
     	migrateCharmsHandler := &charmsHandler{
     		ctxt:          httpCtxt,
     		dataDir:       srv.dataDir,
    @@ -983,7 +984,7 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     	}, {
     		pattern:    "/tools",
     		handler:    modelToolsUploadHandler,
    -		authorizer: modelToolsUploadAuthorizer,
    +		authorizer: controllerAdminAuthorizer,
     	}, {
     		pattern:         "/tools/:version",
     		handler:         modelToolsDownloadHandler,
    
  • apiserver/tools_test.go+23 2 modified
    @@ -24,6 +24,7 @@ import (
     	gc "gopkg.in/check.v1"
     
     	apitesting "github.com/juju/juju/apiserver/testing"
    +	"github.com/juju/juju/core/permission"
     	"github.com/juju/juju/environs"
     	"github.com/juju/juju/environs/simplestreams"
     	"github.com/juju/juju/environs/storage"
    @@ -177,7 +178,7 @@ func (s *toolsSuite) TestRequiresPOST(c *gc.C) {
     	s.assertJSONErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
     }
     
    -func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) {
    +func (s *toolsSuite) TestAuthRejectsNonsUser(c *gc.C) {
     	// Add a machine and try to login.
     	machine, err := s.State.AddMachine(state.UbuntuBase("12.10"), state.JobHostUnits)
     	c.Assert(err, jc.ErrorIsNil)
    @@ -197,14 +198,34 @@ func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) {
     	})
     	s.assertPlainErrorResponse(
     		c, resp, http.StatusForbidden,
    -		"authorization failed: tag kind machine not valid",
    +		"authorization failed: permission denied",
     	)
     
     	// Now try a user login.
     	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")})
     	s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
     }
     
    +func (s *toolsSuite) TestAuthRejectsUserWithoutPermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:     "oryx",
    +		Password: "gardener",
    +		Access:   permission.WriteAccess,
    +	})
    +
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:      u.Tag().String(),
    +		Password: "gardener",
    +		Method:   "POST",
    +		URL:      s.toolsURI(""),
    +		Nonce:    "fake_nonce",
    +	})
    +	s.assertPlainErrorResponse(
    +		c, resp, http.StatusForbidden,
    +		"authorization failed: permission denied",
    +	)
    +}
    +
     func (s *toolsSuite) TestUploadRequiresVersion(c *gc.C) {
     	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")})
     	s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
    
4034aa13c7cf

fix: ensure users uploading tools need admin permission on the model

https://github.com/juju/jujuwallyworldJun 18, 2025via ghsa
2 files changed · +33 4
  • apiserver/apiserver.go+10 2 modified
    @@ -752,7 +752,15 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		ctxt:          httpCtxt,
     		stateAuthFunc: httpCtxt.stateForRequestAuthenticatedUser,
     	}
    -	modelToolsUploadAuthorizer := tagKindAuthorizer{names.UserTagKind}
    +	var modelToolsUploadAuthorizer httpcontext.CompositeAuthorizer = []httpcontext.Authorizer{
    +		controllerAdminAuthorizer{
    +			st: systemState,
    +		},
    +		modelPermissionAuthorizer{
    +			userAccess: systemState.UserPermission,
    +			perm:       permission.AdminAccess,
    +		},
    +	}
     	modelToolsDownloadHandler := newToolsDownloadHandler(httpCtxt)
     	resourcesHandler := &ResourcesHandler{
     		StateAuthFunc: func(req *http.Request, tagKinds ...string) (ResourcesBackend, state.PoolHelper, names.Tag, error) {
    @@ -937,7 +945,7 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     	}, {
     		pattern:    "/tools",
     		handler:    modelToolsUploadHandler,
    -		authorizer: modelToolsUploadAuthorizer,
    +		authorizer: controllerAdminAuthorizer,
     	}, {
     		pattern:         "/tools/:version",
     		handler:         modelToolsDownloadHandler,
    
  • apiserver/tools_test.go+23 2 modified
    @@ -24,6 +24,7 @@ import (
     	gc "gopkg.in/check.v1"
     
     	apitesting "github.com/juju/juju/apiserver/testing"
    +	"github.com/juju/juju/core/permission"
     	"github.com/juju/juju/environs"
     	"github.com/juju/juju/environs/simplestreams"
     	"github.com/juju/juju/environs/storage"
    @@ -177,7 +178,7 @@ func (s *toolsSuite) TestRequiresPOST(c *gc.C) {
     	s.assertJSONErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`)
     }
     
    -func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) {
    +func (s *toolsSuite) TestAuthRejectsNonsUser(c *gc.C) {
     	// Add a machine and try to login.
     	machine, err := s.State.AddMachine("quantal", state.JobHostUnits)
     	c.Assert(err, jc.ErrorIsNil)
    @@ -197,14 +198,34 @@ func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) {
     	})
     	s.assertPlainErrorResponse(
     		c, resp, http.StatusForbidden,
    -		"authorization failed: tag kind machine not valid",
    +		"authorization failed: permission denied",
     	)
     
     	// Now try a user login.
     	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")})
     	s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
     }
     
    +func (s *toolsSuite) TestAuthRejectsUserWithoutPermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:     "oryx",
    +		Password: "gardener",
    +		Access:   permission.WriteAccess,
    +	})
    +
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:      u.Tag().String(),
    +		Password: "gardener",
    +		Method:   "POST",
    +		URL:      s.toolsURI(""),
    +		Nonce:    "fake_nonce",
    +	})
    +	s.assertPlainErrorResponse(
    +		c, resp, http.StatusForbidden,
    +		"authorization failed: permission denied",
    +	)
    +}
    +
     func (s *toolsSuite) TestUploadRequiresVersion(c *gc.C) {
     	resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")})
     	s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument")
    
311e374cb8d2

fix: ensure users uploading charms need write permission on the model

https://github.com/juju/jujuwallyworldJun 17, 2025via ghsa
2 files changed · +61 3
  • apiserver/apiserver.go+8 1 modified
    @@ -756,7 +756,14 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		PostHandler: modelCharmsHandler.ServePost,
     		GetHandler:  modelCharmsHandler.ServeGet,
     	}, "charms")
    -	modelCharmsUploadAuthorizer := tagKindAuthorizer{names.UserTagKind}
    +	var modelCharmsUploadAuthorizer httpcontext.CompositeAuthorizer = []authentication.Authorizer{
    +		controllerAdminAuthorizer{
    +			controllerTag: systemState.ControllerTag(),
    +		},
    +		modelPermissionAuthorizer{
    +			perm: permission.WriteAccess,
    +		},
    +	}
     
     	modelObjectsCharmsHandler := &objectsCharmHandler{
     		ctxt:          httpCtxt,
    
  • apiserver/charms_test.go+53 2 modified
    @@ -23,6 +23,7 @@ import (
     
     	"github.com/juju/juju/apiserver/common"
     	apitesting "github.com/juju/juju/apiserver/testing"
    +	"github.com/juju/juju/core/permission"
     	jujutesting "github.com/juju/juju/juju/testing"
     	"github.com/juju/juju/rpc/params"
     	"github.com/juju/juju/state"
    @@ -128,7 +129,7 @@ func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
     	c.Assert(string(body), gc.Equals, "Method Not Allowed\n")
     }
     
    -func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
    +func (s *charmsSuite) TestPOSTRejectsNonUserAuth(c *gc.C) {
     	// Add a machine and try to login.
     	machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
     		Nonce: "noncy",
    @@ -142,13 +143,63 @@ func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
     		ContentType: "foo/bar",
     	})
     	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
    -	c.Assert(string(body), gc.Equals, "authorization failed: tag kind machine not valid\n")
    +	c.Assert(string(body), gc.Equals, "authorization failed: permission denied\n")
     
     	// Now try a user login.
     	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
     	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
     }
     
    +func (s *charmsSuite) TestPOSTRejectsUserWithoutPermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:        "oryx",
    +		Password:    "gardener",
    +		NoModelUser: true,
    +	})
    +
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:         u.Tag().String(),
    +		Password:    "gardener",
    +		Method:      "POST",
    +		URL:         s.charmsURI(""),
    +		Nonce:       "noncy",
    +		ContentType: "foo/bar",
    +	})
    +	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
    +	c.Assert(string(body), gc.Equals, "authorization failed: permission denied\n")
    +
    +	// Now try a user login.
    +	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
    +	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
    +}
    +
    +func (s *charmsSuite) TestPOSTAllowsUserWithWritePermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:     "oryx",
    +		Password: "gardener",
    +		Access:   permission.WriteAccess,
    +	})
    +
    +	pathToArchive := testcharms.Repo.CharmArchivePath(c.MkDir(), "dummy")
    +	ch, err := charm.ReadCharmArchive(pathToArchive)
    +	c.Assert(err, gc.IsNil)
    +	f, err := os.Open(ch.Path)
    +	c.Assert(err, jc.ErrorIsNil)
    +	defer f.Close()
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:         u.Tag().String(),
    +		Password:    "gardener",
    +		Method:      "POST",
    +		URL:         s.charmsURI("?series=quantal"),
    +		Nonce:       "noncy",
    +		ContentType: "application/zip",
    +		Body:        f,
    +	})
    +
    +	inputURL := charm.MustParseURL("local:quantal/dummy-1")
    +	s.assertUploadResponse(c, resp, inputURL.String())
    +}
    +
     func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
     	var empty bytes.Buffer
     
    
b4176e6e45c2

fix: ensure users uploading charms need write permission on the model

https://github.com/juju/jujuwallyworldJun 17, 2025via ghsa
2 files changed · +62 3
  • apiserver/apiserver.go+9 1 modified
    @@ -739,7 +739,15 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) {
     		PostHandler: modelCharmsHandler.ServePost,
     		GetHandler:  modelCharmsHandler.ServeGet,
     	}
    -	modelCharmsUploadAuthorizer := tagKindAuthorizer{names.UserTagKind}
    +	var modelCharmsUploadAuthorizer httpcontext.CompositeAuthorizer = []httpcontext.Authorizer{
    +		controllerAdminAuthorizer{
    +			st: systemState,
    +		},
    +		modelPermissionAuthorizer{
    +			userAccess: systemState.UserPermission,
    +			perm:       permission.WriteAccess,
    +		},
    +	}
     	modelToolsUploadHandler := &toolsUploadHandler{
     		ctxt:          httpCtxt,
     		stateAuthFunc: httpCtxt.stateForRequestAuthenticatedUser,
    
  • apiserver/charms_test.go+53 2 modified
    @@ -26,6 +26,7 @@ import (
     	"github.com/juju/juju/apiserver/common"
     	apitesting "github.com/juju/juju/apiserver/testing"
     	"github.com/juju/juju/controller"
    +	"github.com/juju/juju/core/permission"
     	"github.com/juju/juju/feature"
     	jujutesting "github.com/juju/juju/juju/testing"
     	"github.com/juju/juju/rpc/params"
    @@ -136,7 +137,7 @@ func (s *charmsSuite) TestRequiresPOSTorGET(c *gc.C) {
     	c.Assert(string(body), gc.Equals, "Method Not Allowed\n")
     }
     
    -func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
    +func (s *charmsSuite) TestPOSTRejectsNonUserAuth(c *gc.C) {
     	// Add a machine and try to login.
     	machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{
     		Nonce: "noncy",
    @@ -150,13 +151,63 @@ func (s *charmsSuite) TestPOSTRequiresUserAuth(c *gc.C) {
     		ContentType: "foo/bar",
     	})
     	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
    -	c.Assert(string(body), gc.Equals, "authorization failed: tag kind machine not valid\n")
    +	c.Assert(string(body), gc.Equals, "authorization failed: permission denied\n")
     
     	// Now try a user login.
     	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
     	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
     }
     
    +func (s *charmsSuite) TestPOSTRejectsUserWithoutPermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:        "oryx",
    +		Password:    "gardener",
    +		NoModelUser: true,
    +	})
    +
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:         u.Tag().String(),
    +		Password:    "gardener",
    +		Method:      "POST",
    +		URL:         s.charmsURI(""),
    +		Nonce:       "noncy",
    +		ContentType: "foo/bar",
    +	})
    +	body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8")
    +	c.Assert(string(body), gc.Equals, "authorization failed: permission denied\n")
    +
    +	// Now try a user login.
    +	resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.charmsURI("")})
    +	s.assertErrorResponse(c, resp, http.StatusBadRequest, ".*expected Content-Type: application/zip.+")
    +}
    +
    +func (s *charmsSuite) TestPOSTAllowsUserWithWritePermission(c *gc.C) {
    +	u := s.Factory.MakeUser(c, &factory.UserParams{
    +		Name:     "oryx",
    +		Password: "gardener",
    +		Access:   permission.WriteAccess,
    +	})
    +
    +	pathToArchive := testcharms.Repo.CharmArchivePath(c.MkDir(), "dummy")
    +	ch, err := charm.ReadCharmArchive(pathToArchive)
    +	c.Assert(err, gc.IsNil)
    +	f, err := os.Open(ch.Path)
    +	c.Assert(err, jc.ErrorIsNil)
    +	defer f.Close()
    +	resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{
    +		Tag:         u.Tag().String(),
    +		Password:    "gardener",
    +		Method:      "POST",
    +		URL:         s.charmsURI("?series=quantal"),
    +		Nonce:       "noncy",
    +		ContentType: "application/zip",
    +		Body:        f,
    +	})
    +
    +	inputURL := charm.MustParseURL("local:quantal/dummy-1")
    +	s.assertUploadResponse(c, resp, inputURL.String())
    +}
    +
     func (s *charmsSuite) TestUploadFailsWithInvalidZip(c *gc.C) {
     	var empty bytes.Buffer
     
    

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

8

News mentions

0

No linked articles in our index yet.