VYPR
Moderate severityNVD Advisory· Published Apr 4, 2024· Updated Nov 6, 2024

CVE-2024-3250

CVE-2024-3250

Description

Canonical Pebble before v1.10.2 allowed unprivileged local users to read arbitrary files as root via the read-file API and pebble pull command.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Canonical Pebble before v1.10.2 allowed unprivileged local users to read arbitrary files as root via the read-file API and pebble pull command.

Vulnerability

Description

CVE-2024-3250 is a privilege-escalation vulnerability in Canonical's Pebble service manager. The flaw exists in the read-file API and the associated pebble pull command, which, prior to version 1.10.2, did not enforce administrative access controls. When Pebble runs as root, unprivileged local users could exploit this missing authorization to read files with root-equivalent permissions, bypassing intended access restrictions. [1]

Attack

Vector and Prerequisites

The vulnerability is exploitable locally by an unprivileged user who can communicate with the Pebble daemon's REST API (typically through the Unix socket). The attacker must be able to make HTTP requests to the /v1/files endpoint. The fix, implemented in commit a5f6f062, explicitly requires admin access (UID 0 or appropriate peer credential) for this API. The commit message and test code confirm that even reading files via the file pull API now demands admin access, closing the privilege gap. [2][3][4]

Impact

An attacker who successfully exploits this vulnerability can read any file on the system to which the root user has access, including sensitive configuration files, cryptographic keys, or other protected data. This represents a complete breach of confidentiality and can serve as a stepping stone for further privilege escalation or lateral movement within the host environment.

Mitigation

The issue has been patched in Pebble v1.10.2. Canonical has also provided backport fixes for versions v1.1.1, v1.4.2, and v1.7.4. Users should upgrade their Pebble installation to one of the fixed versions immediately. No workarounds are documented, but restricting local access to the Pebble socket can reduce the attack surface. [1]

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/canonical/pebbleGo
>= 1.2.0, < 1.4.21.4.2
github.com/canonical/pebbleGo
>= 1.5.0, < 1.7.31.7.3
github.com/canonical/pebbleGo
>= 1.8.0, < 1.10.21.10.2
github.com/canonical/pebbleGo
< 1.1.11.1.1

Affected products

3

Patches

7
a5f6f062a11e

fix(daemon): require admin access for POSTs and file pull API (#406)

https://github.com/canonical/pebbleBen HoytApr 3, 2024via ghsa
2 files changed · +134 11
  • internals/daemon/api.go+11 11 modified
    @@ -35,7 +35,7 @@ var API = []*Command{{
     }, {
     	Path:        "/v1/warnings",
     	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	GET:         v1GetWarnings,
     	POST:        v1AckWarnings,
     }, {
    @@ -45,7 +45,7 @@ var API = []*Command{{
     }, {
     	Path:        "/v1/changes/{id}",
     	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	GET:         v1GetChange,
     	POST:        v1PostChange,
     }, {
    @@ -55,13 +55,13 @@ var API = []*Command{{
     }, {
     	Path:        "/v1/services",
     	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	GET:         v1GetServices,
     	POST:        v1PostServices,
     }, {
     	Path:        "/v1/services/{name}",
     	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	GET:         v1GetService,
     	POST:        v1PostService,
     }, {
    @@ -70,12 +70,12 @@ var API = []*Command{{
     	GET:        v1GetPlan,
     }, {
     	Path:        "/v1/layers",
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	POST:        v1PostLayers,
     }, {
     	Path:        "/v1/files",
    -	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	ReadAccess:  AdminAccess{}, // some files are sensitive, so require admin
    +	WriteAccess: AdminAccess{},
     	GET:         v1GetFiles,
     	POST:        v1PostFiles,
     }, {
    @@ -84,15 +84,15 @@ var API = []*Command{{
     	GET:        v1GetLogs,
     }, {
     	Path:        "/v1/exec",
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	POST:        v1PostExec,
     }, {
     	Path:       "/v1/tasks/{task-id}/websocket/{websocket-id}",
    -	ReadAccess: UserAccess{},
    +	ReadAccess: AdminAccess{}, // used by exec, so require admin
     	GET:        v1GetTaskWebsocket,
     }, {
     	Path:        "/v1/signals",
    -	WriteAccess: UserAccess{},
    +	WriteAccess: AdminAccess{},
     	POST:        v1PostSignals,
     }, {
     	Path:       "/v1/checks",
    @@ -101,7 +101,7 @@ var API = []*Command{{
     }, {
     	Path:        "/v1/notices",
     	ReadAccess:  UserAccess{},
    -	WriteAccess: UserAccess{},
    +	WriteAccess: UserAccess{}, // any user is allowed to add a notice with their own uid
     	GET:         v1GetNotices,
     	POST:        v1PostNotices,
     }, {
    
  • internals/daemon/daemon_test.go+123 0 modified
    @@ -22,8 +22,10 @@ import (
     	"net"
     	"net/http"
     	"net/http/httptest"
    +	"net/url"
     	"os"
     	"path/filepath"
    +	"strings"
     	"sync"
     	"syscall"
     	"testing"
    @@ -1248,6 +1250,127 @@ services:
     	c.Check(tasks[0].Kind(), Equals, "stop")
     }
     
    +func (s *daemonSuite) TestWritesRequireAdminAccess(c *C) {
    +	for _, cmd := range API {
    +		if cmd.Path == "/v1/notices" {
    +			// Any user is allowed to add a notice with their own uid.
    +			continue
    +		}
    +		switch cmd.WriteAccess.(type) {
    +		case OpenAccess, UserAccess:
    +			c.Errorf("%s WriteAccess should be AdminAccess, not %T", cmd.Path, cmd.WriteAccess)
    +		}
    +	}
    +
    +	// File pull (read) may be sensitive, so requires admin access too.
    +	cmd := apiCmd("/v1/files")
    +	switch cmd.ReadAccess.(type) {
    +	case OpenAccess, UserAccess:
    +		c.Errorf("%s ReadAccess should be AdminAccess, not %T", cmd.Path, cmd.WriteAccess)
    +	}
    +
    +	// Task websockets (GET) is used for exec, so requires admin access too.
    +	cmd = apiCmd("/v1/tasks/{task-id}/websocket/{websocket-id}")
    +	switch cmd.ReadAccess.(type) {
    +	case OpenAccess, UserAccess:
    +		c.Errorf("%s ReadAccess should be AdminAccess, not %T", cmd.Path, cmd.WriteAccess)
    +	}
    +}
    +
    +func (s *daemonSuite) TestAPIAccessLevels(c *C) {
    +	_ = s.newDaemon(c)
    +
    +	tests := []struct {
    +		method string
    +		path   string
    +		body   string
    +		uid    int // -1 means no peer cred user
    +		status int
    +	}{
    +		{"GET", "/v1/system-info", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/health", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/warnings", ``, 42, http.StatusOK},
    +		{"GET", "/v1/warnings", ``, 0, http.StatusOK},
    +		{"POST", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/changes", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/changes", ``, 42, http.StatusOK},
    +		{"GET", "/v1/changes", ``, 0, http.StatusOK},
    +
    +		{"GET", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/services", ``, 42, http.StatusOK},
    +		{"GET", "/v1/services", ``, 0, http.StatusOK},
    +		{"POST", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/layers", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/files?action=list&path=/", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/files?action=list&path=/", ``, 42, http.StatusUnauthorized}, // even reading files requires admin
    +		{"GET", "/v1/files?action=list&path=/", ``, 0, http.StatusOK},
    +		{"POST", "/v1/files", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/logs", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/logs", ``, 42, http.StatusOK},
    +		{"GET", "/v1/logs", ``, 0, http.StatusOK},
    +
    +		{"POST", "/v1/exec", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/signals", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/checks", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/checks", ``, 42, http.StatusOK},
    +		{"GET", "/v1/checks", ``, 0, http.StatusOK},
    +
    +		{"GET", "/v1/notices", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/notices", ``, 42, http.StatusOK},
    +		{"GET", "/v1/notices", ``, 0, http.StatusOK},
    +		{"POST", "/v1/notices", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/notices", `{}`, 42, http.StatusBadRequest},
    +		{"POST", "/v1/notices", `{}`, 0, http.StatusBadRequest},
    +	}
    +
    +	for _, test := range tests {
    +		remoteAddr := ""
    +		if test.uid >= 0 {
    +			remoteAddr = fmt.Sprintf("pid=100;uid=%d;socket=;", test.uid)
    +		}
    +		requestURL, err := url.Parse("http://localhost" + test.path)
    +		c.Assert(err, IsNil)
    +		request := &http.Request{
    +			Method:     test.method,
    +			URL:        requestURL,
    +			Body:       io.NopCloser(strings.NewReader(test.body)),
    +			RemoteAddr: remoteAddr,
    +		}
    +		recorder := httptest.NewRecorder()
    +		cmd := apiCmd(requestURL.Path)
    +		cmd.ServeHTTP(recorder, request)
    +
    +		response := recorder.Result()
    +		if response.StatusCode != test.status {
    +			// Log response body to make it easier to debug if the test fails.
    +			c.Logf("%s %s uid=%d: expected %d, got %d; response body:\n%s",
    +				test.method, test.path, test.uid, test.status, response.StatusCode, recorder.Body.String())
    +		}
    +		c.Assert(response.StatusCode, Equals, test.status)
    +	}
    +}
    +
     type rebootSuite struct{}
     
     var _ = Suite(&rebootSuite{})
    
4ca343d38895

fix(daemon): require admin access for file pull API

https://github.com/canonical/pebbleBen HoytApr 2, 2024via ghsa
2 files changed · +97 7
  • internals/daemon/api.go+7 7 modified
    @@ -70,10 +70,10 @@ var API = []*Command{{
     	UserOK: true,
     	POST:   v1PostLayers,
     }, {
    -	Path:   "/v1/files",
    -	UserOK: true,
    -	GET:    v1GetFiles,
    -	POST:   v1PostFiles,
    +	Path:      "/v1/files",
    +	AdminOnly: true,
    +	GET:       v1GetFiles,
    +	POST:      v1PostFiles,
     }, {
     	Path:   "/v1/logs",
     	UserOK: true,
    @@ -83,9 +83,9 @@ var API = []*Command{{
     	UserOK: true,
     	POST:   v1PostExec,
     }, {
    -	Path:   "/v1/tasks/{task-id}/websocket/{websocket-id}",
    -	UserOK: true,
    -	GET:    v1GetTaskWebsocket,
    +	Path:      "/v1/tasks/{task-id}/websocket/{websocket-id}",
    +	AdminOnly: true,
    +	GET:       v1GetTaskWebsocket,
     }, {
     	Path:   "/v1/signals",
     	UserOK: true,
    
  • internals/daemon/daemon_test.go+90 0 modified
    @@ -18,12 +18,15 @@ import (
     	"bytes"
     	"encoding/json"
     	"fmt"
    +	"io"
     	"io/ioutil"
     	"net"
     	"net/http"
     	"net/http/httptest"
    +	"net/url"
     	"os"
     	"path/filepath"
    +	"strings"
     	"sync"
     	"syscall"
     	"testing"
    @@ -1292,6 +1295,93 @@ services:
     	c.Check(tasks[0].Kind(), Equals, "stop")
     }
     
    +func (s *daemonSuite) TestAPIAccessLevels(c *C) {
    +	_ = s.newDaemon(c)
    +
    +	tests := []struct {
    +		method string
    +		path   string
    +		body   string
    +		uid    int // -1 means no peer cred user
    +		status int
    +	}{
    +		{"GET", "/v1/system-info", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/health", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/warnings", ``, 42, http.StatusOK},
    +		{"GET", "/v1/warnings", ``, 0, http.StatusOK},
    +		{"POST", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/changes", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/changes", ``, 42, http.StatusOK},
    +		{"GET", "/v1/changes", ``, 0, http.StatusOK},
    +
    +		{"GET", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/services", ``, 42, http.StatusOK},
    +		{"GET", "/v1/services", ``, 0, http.StatusOK},
    +		{"POST", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/layers", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/files?action=list&path=/", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/files?action=list&path=/", ``, 42, http.StatusUnauthorized}, // even reading files requires admin
    +		{"GET", "/v1/files?action=list&path=/", ``, 0, http.StatusOK},
    +		{"POST", "/v1/files", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/logs", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/logs", ``, 42, http.StatusOK},
    +		{"GET", "/v1/logs", ``, 0, http.StatusOK},
    +
    +		{"POST", "/v1/exec", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/signals", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/checks", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/checks", ``, 42, http.StatusOK},
    +		{"GET", "/v1/checks", ``, 0, http.StatusOK},
    +	}
    +
    +	for _, test := range tests {
    +		remoteAddr := ""
    +		if test.uid >= 0 {
    +			remoteAddr = fmt.Sprintf("pid=100;uid=%d;socket=;", test.uid)
    +		}
    +		requestURL, err := url.Parse("http://localhost" + test.path)
    +		c.Assert(err, IsNil)
    +		request := &http.Request{
    +			Method:     test.method,
    +			URL:        requestURL,
    +			Body:       io.NopCloser(strings.NewReader(test.body)),
    +			RemoteAddr: remoteAddr,
    +		}
    +		recorder := httptest.NewRecorder()
    +		cmd := apiCmd(requestURL.Path)
    +		cmd.ServeHTTP(recorder, request)
    +
    +		response := recorder.Result()
    +		if response.StatusCode != test.status {
    +			// Log response body to make it easier to debug if the test fails.
    +			c.Logf("%s %s uid=%d: expected %d, got %d; response body:\n%s",
    +				test.method, test.path, test.uid, test.status, response.StatusCode, recorder.Body.String())
    +		}
    +		c.Assert(response.StatusCode, Equals, test.status)
    +	}
    +}
    +
     type rebootSuite struct{}
     
     var _ = Suite(&rebootSuite{})
    
cd326225b9b0

fix(daemon): require admin access for file pull API

https://github.com/canonical/pebbleBen HoytApr 2, 2024via ghsa
2 files changed · +97 7
  • internal/daemon/api.go+7 7 modified
    @@ -70,10 +70,10 @@ var api = []*Command{{
     	UserOK: true,
     	POST:   v1PostLayers,
     }, {
    -	Path:   "/v1/files",
    -	UserOK: true,
    -	GET:    v1GetFiles,
    -	POST:   v1PostFiles,
    +	Path:      "/v1/files",
    +	AdminOnly: true,
    +	GET:       v1GetFiles,
    +	POST:      v1PostFiles,
     }, {
     	Path:   "/v1/logs",
     	UserOK: true,
    @@ -83,9 +83,9 @@ var api = []*Command{{
     	UserOK: true,
     	POST:   v1PostExec,
     }, {
    -	Path:   "/v1/tasks/{task-id}/websocket/{websocket-id}",
    -	UserOK: true,
    -	GET:    v1GetTaskWebsocket,
    +	Path:      "/v1/tasks/{task-id}/websocket/{websocket-id}",
    +	AdminOnly: true,
    +	GET:       v1GetTaskWebsocket,
     }, {
     	Path:   "/v1/signals",
     	UserOK: true,
    
  • internal/daemon/daemon_test.go+90 0 modified
    @@ -18,12 +18,15 @@ import (
     	"bytes"
     	"encoding/json"
     	"fmt"
    +	"io"
     	"io/ioutil"
     	"net"
     	"net/http"
     	"net/http/httptest"
    +	"net/url"
     	"os"
     	"path/filepath"
    +	"strings"
     	"sync"
     	"syscall"
     	"testing"
    @@ -1148,3 +1151,90 @@ services:
     	c.Assert(tasks, HasLen, 1)
     	c.Check(tasks[0].Kind(), Equals, "stop")
     }
    +
    +func (s *daemonSuite) TestAPIAccessLevels(c *C) {
    +	_ = s.newDaemon(c)
    +
    +	tests := []struct {
    +		method string
    +		path   string
    +		body   string
    +		uid    int // -1 means no peer cred user
    +		status int
    +	}{
    +		{"GET", "/v1/system-info", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/health", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/warnings", ``, 42, http.StatusOK},
    +		{"GET", "/v1/warnings", ``, 0, http.StatusOK},
    +		{"POST", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/changes", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/changes", ``, 42, http.StatusOK},
    +		{"GET", "/v1/changes", ``, 0, http.StatusOK},
    +
    +		{"GET", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/services", ``, 42, http.StatusOK},
    +		{"GET", "/v1/services", ``, 0, http.StatusOK},
    +		{"POST", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/layers", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/files?action=list&path=/", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/files?action=list&path=/", ``, 42, http.StatusUnauthorized}, // even reading files requires admin
    +		{"GET", "/v1/files?action=list&path=/", ``, 0, http.StatusOK},
    +		{"POST", "/v1/files", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/logs", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/logs", ``, 42, http.StatusOK},
    +		{"GET", "/v1/logs", ``, 0, http.StatusOK},
    +
    +		{"POST", "/v1/exec", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/signals", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/checks", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/checks", ``, 42, http.StatusOK},
    +		{"GET", "/v1/checks", ``, 0, http.StatusOK},
    +	}
    +
    +	for _, test := range tests {
    +		remoteAddr := ""
    +		if test.uid >= 0 {
    +			remoteAddr = fmt.Sprintf("pid=100;uid=%d;socket=;", test.uid)
    +		}
    +		requestURL, err := url.Parse("http://localhost" + test.path)
    +		c.Assert(err, IsNil)
    +		request := &http.Request{
    +			Method:     test.method,
    +			URL:        requestURL,
    +			Body:       io.NopCloser(strings.NewReader(test.body)),
    +			RemoteAddr: remoteAddr,
    +		}
    +		recorder := httptest.NewRecorder()
    +		cmd := apiCmd(requestURL.Path)
    +		cmd.ServeHTTP(recorder, request)
    +
    +		response := recorder.Result()
    +		if response.StatusCode != test.status {
    +			// Log response body to make it easier to debug if the test fails.
    +			c.Logf("%s %s uid=%d: expected %d, got %d; response body:\n%s",
    +				test.method, test.path, test.uid, test.status, response.StatusCode, recorder.Body.String())
    +		}
    +		c.Assert(response.StatusCode, Equals, test.status)
    +	}
    +}
    
b8abd1ff0090

fix(daemon): require admin access for file pull API

https://github.com/canonical/pebbleBen HoytApr 2, 2024via ghsa
2 files changed · +97 7
  • internals/daemon/api.go+7 7 modified
    @@ -70,10 +70,10 @@ var API = []*Command{{
     	UserOK: true,
     	POST:   v1PostLayers,
     }, {
    -	Path:   "/v1/files",
    -	UserOK: true,
    -	GET:    v1GetFiles,
    -	POST:   v1PostFiles,
    +	Path:      "/v1/files",
    +	AdminOnly: true,
    +	GET:       v1GetFiles,
    +	POST:      v1PostFiles,
     }, {
     	Path:   "/v1/logs",
     	UserOK: true,
    @@ -83,9 +83,9 @@ var API = []*Command{{
     	UserOK: true,
     	POST:   v1PostExec,
     }, {
    -	Path:   "/v1/tasks/{task-id}/websocket/{websocket-id}",
    -	UserOK: true,
    -	GET:    v1GetTaskWebsocket,
    +	Path:      "/v1/tasks/{task-id}/websocket/{websocket-id}",
    +	AdminOnly: true,
    +	GET:       v1GetTaskWebsocket,
     }, {
     	Path:   "/v1/signals",
     	UserOK: true,
    
  • internals/daemon/daemon_test.go+90 0 modified
    @@ -18,12 +18,15 @@ import (
     	"bytes"
     	"encoding/json"
     	"fmt"
    +	"io"
     	"io/ioutil"
     	"net"
     	"net/http"
     	"net/http/httptest"
    +	"net/url"
     	"os"
     	"path/filepath"
    +	"strings"
     	"sync"
     	"syscall"
     	"testing"
    @@ -1243,6 +1246,93 @@ services:
     	c.Check(tasks[0].Kind(), Equals, "stop")
     }
     
    +func (s *daemonSuite) TestAPIAccessLevels(c *C) {
    +	_ = s.newDaemon(c)
    +
    +	tests := []struct {
    +		method string
    +		path   string
    +		body   string
    +		uid    int // -1 means no peer cred user
    +		status int
    +	}{
    +		{"GET", "/v1/system-info", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/health", ``, -1, http.StatusOK},
    +
    +		{"GET", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/warnings", ``, 42, http.StatusOK},
    +		{"GET", "/v1/warnings", ``, 0, http.StatusOK},
    +		{"POST", "/v1/warnings", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/warnings", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/changes", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/changes", ``, 42, http.StatusOK},
    +		{"GET", "/v1/changes", ``, 0, http.StatusOK},
    +
    +		{"GET", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/services", ``, 42, http.StatusOK},
    +		{"GET", "/v1/services", ``, 0, http.StatusOK},
    +		{"POST", "/v1/services", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/services", ``, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/layers", ``, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/layers", ``, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/files?action=list&path=/", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/files?action=list&path=/", ``, 42, http.StatusUnauthorized}, // even reading files requires admin
    +		{"GET", "/v1/files?action=list&path=/", ``, 0, http.StatusOK},
    +		{"POST", "/v1/files", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/files", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/logs", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/logs", ``, 42, http.StatusOK},
    +		{"GET", "/v1/logs", ``, 0, http.StatusOK},
    +
    +		{"POST", "/v1/exec", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/exec", `{}`, 0, http.StatusBadRequest},
    +
    +		{"POST", "/v1/signals", `{}`, -1, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 42, http.StatusUnauthorized},
    +		{"POST", "/v1/signals", `{}`, 0, http.StatusBadRequest},
    +
    +		{"GET", "/v1/checks", ``, -1, http.StatusUnauthorized},
    +		{"GET", "/v1/checks", ``, 42, http.StatusOK},
    +		{"GET", "/v1/checks", ``, 0, http.StatusOK},
    +	}
    +
    +	for _, test := range tests {
    +		remoteAddr := ""
    +		if test.uid >= 0 {
    +			remoteAddr = fmt.Sprintf("pid=100;uid=%d;socket=;", test.uid)
    +		}
    +		requestURL, err := url.Parse("http://localhost" + test.path)
    +		c.Assert(err, IsNil)
    +		request := &http.Request{
    +			Method:     test.method,
    +			URL:        requestURL,
    +			Body:       io.NopCloser(strings.NewReader(test.body)),
    +			RemoteAddr: remoteAddr,
    +		}
    +		recorder := httptest.NewRecorder()
    +		cmd := apiCmd(requestURL.Path)
    +		cmd.ServeHTTP(recorder, request)
    +
    +		response := recorder.Result()
    +		if response.StatusCode != test.status {
    +			// Log response body to make it easier to debug if the test fails.
    +			c.Logf("%s %s uid=%d: expected %d, got %d; response body:\n%s",
    +				test.method, test.path, test.uid, test.status, response.StatusCode, recorder.Body.String())
    +		}
    +		c.Assert(response.StatusCode, Equals, test.status)
    +	}
    +}
    +
     type rebootSuite struct{}
     
     var _ = Suite(&rebootSuite{})
    

Vulnerability mechanics

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

References

9

News mentions

0

No linked articles in our index yet.