Moderate severityNVD Advisory· Published Jan 31, 2025· Updated Feb 7, 2025
CVE-2023-0092
CVE-2023-0092
Description
An authenticated user who has read access to the juju controller model, may construct a remote request to download an arbitrary file from the controller's filesystem.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/juju/jujuGo | >= 2.9.22, < 2.9.38 | 2.9.38 |
github.com/juju/jujuGo | >= 3.0.0, < 3.0.3 | 3.0.3 |
Affected products
1- Range: 2.9.22
Patches
1ef803e2a1369Check backup filename when downloading
9 files changed · +96 −37
apiserver/apiserver.go+3 −2 modified@@ -864,8 +864,9 @@ func (srv *Server) endpoints() ([]apihttp.Endpoint, error) { pattern: modelRoutePrefix + "/units/:unit/resources/:resource", handler: unitResourcesHandler, }, { - pattern: modelRoutePrefix + "/backups", - handler: backupHandler, + pattern: modelRoutePrefix + "/backups", + handler: backupHandler, + authorizer: controllerAdminAuthorizer, }, { pattern: "/migrate/charms", handler: migrateCharmsHTTPHandler,
apiserver/backup.go+20 −1 modified@@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "net/http" + "os" "github.com/juju/errors" @@ -41,7 +42,25 @@ func (h *backupHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { switch req.Method { case "GET": logger.Infof("handling backups download request") - id, err := h.download(newBackups(), resp, req) + model, err := st.Model() + if err != nil { + h.sendError(resp, err) + return + } + modelConfig, err := model.ModelConfig() + if err != nil { + h.sendError(resp, err) + return + } + backupDir := modelConfig.BackupDir() + if backupDir == "" { + backupDir = os.TempDir() + } + + paths := &backups.Paths{ + BackupDir: backupDir, + } + id, err := h.download(newBackups(paths), resp, req) if err != nil { h.sendError(resp, err) return
apiserver/backup_test.go+5 −2 modified@@ -37,7 +37,7 @@ func (s *backupsSuite) SetUpTest(c *gc.C) { s.backupURL = s.server.URL + fmt.Sprintf("/model/%s/backups", s.State.ModelUUID()) s.fake = &backupstesting.FakeBackups{} s.PatchValue(apiserver.NewBackups, - func() backups.Backups { + func(path *backups.Paths) backups.Backups { return s.fake }, ) @@ -98,7 +98,10 @@ func (s *backupsSuite) TestAuthRequiresClientNotMachine(c *gc.C) { URL: s.backupURL, Nonce: "fake_nonce", }) - s.assertErrorResponse(c, resp, http.StatusInternalServerError, "tag kind machine not valid") + c.Assert(resp.StatusCode, gc.Equals, http.StatusForbidden) + body, err := ioutil.ReadAll(resp.Body) + c.Assert(err, jc.ErrorIsNil) + c.Assert(string(body), gc.Equals, "authorization failed: machine 0 is not a user\n") // Now try a user login. resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.backupURL})
apiserver/facades/client/backups/backups.go+5 −0 modified@@ -4,6 +4,8 @@ package backups import ( + "os" + "github.com/juju/errors" "github.com/juju/mgo/v2" "github.com/juju/names/v4" @@ -109,6 +111,9 @@ func NewAPI(backend Backend, resources facade.Resources, authorizer facade.Autho return nil, errors.Trace(err) } backupDir := modelConfig.BackupDir() + if backupDir == "" { + backupDir = os.TempDir() + } paths := backups.Paths{ BackupDir: backupDir,
apiserver/facades/client/backups/backups_test.go+1 −1 modified@@ -78,7 +78,7 @@ func (s *backupsSuite) setBackups(c *gc.C, meta *backups.Metadata, err string) * fake.Error = errors.Errorf(err) } s.PatchValue(backupsAPI.NewBackups, - func() backups.Backups { + func(paths *backups.Paths) backups.Backups { return &fake }, )
apiserver/facades/client/backups/create.go+2 −2 modified@@ -33,7 +33,7 @@ func (a *API) Create(args params.BackupsCreateArgs) (params.BackupsMetadataResul } func (a *APIv2) Create(args params.BackupsCreateArgs) (params.BackupsMetadataResult, error) { - backupsMethods := newBackups() + backupsMethods := newBackups(a.paths) session := a.backend.MongoSession().Copy() defer session.Close() @@ -88,7 +88,7 @@ func (a *APIv2) Create(args params.BackupsCreateArgs) (params.BackupsMetadataRes } meta.Controller.HANodes = int64(len(nodes)) - fileName, err := backupsMethods.Create(meta, a.paths, dbInfo) + fileName, err := backupsMethods.Create(meta, dbInfo) if err != nil { return result, errors.Trace(err) }
state/backups/backups.go+41 −11 modified@@ -6,8 +6,10 @@ package backups import ( "fmt" "io" + "io/fs" "os" "path/filepath" + "strings" "time" "github.com/dustin/go-humanize" @@ -46,17 +48,21 @@ var ( type Backups interface { // Create creates a new juju backup archive. It updates // the provided metadata. - Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) (string, error) + Create(meta *Metadata, dbInfo *DBInfo) (string, error) // Get returns the metadata and specified archive file. Get(fileName string) (*Metadata, io.ReadCloser, error) } -type backups struct{} +type backups struct { + paths *Paths +} // NewBackups creates a new Backups value using the FileStorage provided. -func NewBackups() Backups { - return &backups{} +func NewBackups(paths *Paths) Backups { + return &backups{ + paths: paths, + } } func totalDirSize(path string) (int64, error) { @@ -75,7 +81,7 @@ func totalDirSize(path string) (int64, error) { // Create creates and stores a new juju backup archive (based on arguments) // and updates the provided metadata. A filename to download the backup is provided. -func (b *backups) Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) (string, error) { +func (b *backups) Create(meta *Metadata, dbInfo *DBInfo) (string, error) { // TODO(fwereade): 2016-03-17 lp:1558657 meta.Started = time.Now().UTC() @@ -90,7 +96,7 @@ func (b *backups) Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) (string, } // Create the archive. - filesToBackUp, err := getFilesToBackUp("", paths) + filesToBackUp, err := getFilesToBackUp("", b.paths) if err != nil { return "", errors.Annotate(err, "while listing files to back up") } @@ -108,11 +114,7 @@ func (b *backups) Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) (string, logger.Infof("backing up %dMiB (files) and %dMiB (database) = %dMiB", totalFizeSizesMiB, dbInfo.ApproxSizeMB, int(totalFizeSizesMiB)+dbInfo.ApproxSizeMB) - destinationDir := paths.BackupDir - if destinationDir == "" { - destinationDir = os.TempDir() - } - + destinationDir := b.paths.BackupDir if _, err := os.Stat(destinationDir); err != nil { if os.IsNotExist(err) { return "", errors.Errorf("backup destination directory %q does not exist", destinationDir) @@ -171,8 +173,36 @@ func (b *backups) Create(meta *Metadata, paths *Paths, dbInfo *DBInfo) (string, return result.filename, nil } +func isValidFilepath(root string, filePath string) (bool, error) { + if !filepath.IsAbs(filePath) { + return false, nil + } + if !strings.HasPrefix(filepath.Base(filePath), FilenamePrefix) { + return false, nil + } + result := false + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if d.IsDir() { + return nil + } + if path == filePath { + result = true + return nil + } + return nil + }) + return result, err +} + // Get retrieves the associated metadata and archive file a file on the machine. func (b *backups) Get(fileName string) (_ *Metadata, _ io.ReadCloser, err error) { + valid, err := isValidFilepath(b.paths.BackupDir, fileName) + if err != nil { + return nil, nil, errors.Trace(err) + } + if !valid { + return nil, nil, errors.NotValidf("backup file %q", fileName) + } defer func() { // On success, remove the retrieved file. if err != nil {
state/backups/backups_test.go+19 −14 modified@@ -9,6 +9,7 @@ import ( "io/ioutil" "os" "path" + "path/filepath" "github.com/dustin/go-humanize" "github.com/juju/collections/set" @@ -24,7 +25,8 @@ import ( type backupsSuite struct { backupstesting.BaseSuite - api backups.Backups + paths *backups.Paths + api backups.Backups totalDiskMiB uint64 availableDiskMiB uint64 @@ -37,7 +39,11 @@ var _ = gc.Suite(&backupsSuite{}) // Register the suite. func (s *backupsSuite) SetUpTest(c *gc.C) { s.BaseSuite.SetUpTest(c) - s.api = backups.NewBackups() + s.paths = &backups.Paths{ + BackupDir: c.MkDir(), + DataDir: c.MkDir(), + } + s.api = backups.NewBackups(s.paths) s.PatchValue(backups.AvailableDisk, func(string) uint64 { return s.availableDiskMiB }) @@ -60,7 +66,6 @@ func (s *backupsSuite) checkFailure(c *gc.C, expected string) { return &fakeDumper{}, nil }) - paths := backups.Paths{DataDir: "/var/lib/juju"} targets := set.NewStrings("juju", "admin") dbInfo := backups.DBInfo{ Address: "a", Username: "b", Password: "c", @@ -69,20 +74,18 @@ func (s *backupsSuite) checkFailure(c *gc.C, expected string) { meta := backupstesting.NewMetadataStarted() meta.Notes = "some notes" - _, err := s.api.Create(meta, &paths, &dbInfo) + _, err := s.api.Create(meta, &dbInfo) c.Check(err, gc.ErrorMatches, expected) } func (s *backupsSuite) TestCreateOkay(c *gc.C) { - dataDir := c.MkDir() - backupDir := c.MkDir() // Patch the internals. archiveFile := ioutil.NopCloser(bytes.NewBufferString("<compressed tarball>")) result := backups.NewTestCreateResult( archiveFile, 10, "<checksum>", - path.Join(backupDir, "test-backup.tar.gz")) + path.Join(s.paths.BackupDir, "test-backup.tar.gz")) received, testCreate := backups.NewTestCreate(result) s.PatchValue(backups.RunCreate, testCreate) @@ -99,7 +102,6 @@ func (s *backupsSuite) TestCreateOkay(c *gc.C) { }) // Run the backup. - paths := backups.Paths{BackupDir: backupDir, DataDir: dataDir} targets := set.NewStrings("juju", "admin") dbInfo := backups.DBInfo{ Address: "a", Username: "b", Password: "c", @@ -108,13 +110,13 @@ func (s *backupsSuite) TestCreateOkay(c *gc.C) { meta := backupstesting.NewMetadataStarted() backupstesting.SetOrigin(meta, "<model ID>", "<machine ID>", "<hostname>") meta.Notes = "some notes" - resultFilename, err := s.api.Create(meta, &paths, &dbInfo) + resultFilename, err := s.api.Create(meta, &dbInfo) c.Assert(err, jc.ErrorIsNil) - c.Assert(resultFilename, gc.Equals, path.Join(backupDir, "test-backup.tar.gz")) + c.Assert(resultFilename, gc.Equals, path.Join(s.paths.BackupDir, "test-backup.tar.gz")) // Test the call values. resultBackupDir, filesToBackUp, _ := backups.ExposeCreateArgs(received) - c.Check(resultBackupDir, gc.Equals, backupDir) + c.Check(resultBackupDir, gc.Equals, s.paths.BackupDir) c.Check(filesToBackUp, jc.SameContents, []string{"<some file>"}) c.Check(receivedDBInfo.Address, gc.Equals, "a") @@ -198,15 +200,18 @@ func (s *backupsSuite) TestNotEnoughDiskSpaceSmallDisk(c *gc.C) { } func (s *backupsSuite) TestGetFileName(c *gc.C) { - backupDir := c.MkDir() - err := os.MkdirAll(backupDir, 0644) + backupSubDir := filepath.Join(s.paths.BackupDir, "a", "b") + err := os.MkdirAll(backupSubDir, 0755) c.Assert(err, jc.ErrorIsNil) - backupFilename := path.Join(backupDir, "test-backup.tar.gz") + backupFilename := path.Join(backupSubDir, "juju-backup-123.tar.gz") backupFile, err := os.Create(backupFilename) c.Assert(err, jc.ErrorIsNil) _, err = backupFile.Write([]byte("archive file testing")) c.Assert(err, jc.ErrorIsNil) + _, _, err = s.api.Get("/etc/hostname") + c.Assert(err, gc.ErrorMatches, `backup file "/etc/hostname" not valid`) + resultMeta, resultArchive, err := s.api.Get(backupFilename) c.Assert(err, jc.ErrorIsNil) defer resultArchive.Close()
state/backups/testing/fakes.go+0 −4 modified@@ -31,8 +31,6 @@ type FakeBackups struct { // IDArg holds the ID that was passed in. IDArg string - // PathsArg holds the Paths that was passed in. - PathsArg *backups.Paths // DBInfoArg holds the ConnInfo that was passed in. DBInfoArg *backups.DBInfo // MetaArg holds the backup metadata that was passed in. @@ -51,12 +49,10 @@ var _ backups.Backups = (*FakeBackups)(nil) // its associated metadata. func (b *FakeBackups) Create( meta *backups.Metadata, - paths *backups.Paths, dbInfo *backups.DBInfo, ) (string, error) { b.Calls = append(b.Calls, "Create") - b.PathsArg = paths b.DBInfoArg = dbInfo b.MetaArg = meta
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
5- github.com/juju/juju/commit/ef803e2a13692d355b784b7da8b4b1f01dab1556ghsapatchWEB
- github.com/advisories/GHSA-x5rv-w9pm-8qp8ghsaissue-trackingADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-0092ghsaADVISORY
- bugs.launchpad.net/juju/+bug/1999622ghsaWEB
- github.com/juju/juju/security/advisories/GHSA-x5rv-w9pm-8qp8ghsaWEB
News mentions
0No linked articles in our index yet.