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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/canonical/pebbleGo | >= 1.2.0, < 1.4.2 | 1.4.2 |
github.com/canonical/pebbleGo | >= 1.5.0, < 1.7.3 | 1.7.3 |
github.com/canonical/pebbleGo | >= 1.8.0, < 1.10.2 | 1.10.2 |
github.com/canonical/pebbleGo | < 1.1.1 | 1.1.1 |
Affected products
3- osv-coords2 versions
< 1.4.1+ 1 more
- (no CPE)range: < 1.4.1
- (no CPE)range: >= 1.2.0, < 1.4.2
- Canonical Ltd./Pebblev5Range: 0
Patches
7570313701fce06dd1b9d8fc14b5f2540acd7a5f6f062a11efix(daemon): require admin access for POSTs and file pull API (#406)
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{})
4ca343d38895fix(daemon): require admin access for file pull API
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{})
cd326225b9b0fix(daemon): require admin access for file pull API
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) + } +}
b8abd1ff0090fix(daemon): require admin access for file pull API
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- github.com/advisories/GHSA-4685-2x5r-65pjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-3250ghsaADVISORY
- github.com/canonical/pebble/commit/4ca343d3889533143477e21c63867f2f3c3b5645ghsaWEB
- github.com/canonical/pebble/commit/a5f6f062a11ea156697b854264385ff7e1985fd8ghsaWEB
- github.com/canonical/pebble/commit/b8abd1ff0090f3e0749e81eb1fc3ea16ba95f514ghsaWEB
- github.com/canonical/pebble/commit/cd326225b9b0be067da7d8858e2c912078cbbbd5ghsaWEB
- github.com/canonical/pebble/pull/406ghsaWEB
- github.com/canonical/pebble/security/advisories/GHSA-4685-2x5r-65pjghsaissue-trackingWEB
- www.cve.org/CVERecordghsaissue-trackingWEB
News mentions
0No linked articles in our index yet.