CVE-2026-34042
Description
act is a project which allows for local running of github actions. Prior to version 0.2.86, act's built in actions/cache server listens to connections on all interfaces and allows anyone who can connect to it including someone anywhere on the internet to create caches with arbitrary keys and retrieve all existing caches. If they can predict which cache keys will be used by local actions, they can create malicious caches containing whatever files they please most likely allowing arbitrary remote code execution within the docker container. This issue has been patched in version 0.2.86.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/nektos/actGo | < 0.2.86 | 0.2.86 |
Affected products
1Patches
13 files changed · +56 −17
pkg/artifactcache/doc.go+0 −1 modified@@ -2,7 +2,6 @@ // // Inspired by https://github.com/sp-ricard-valverde/github-act-cache-server // -// TODO: Authorization // TODO: Restrictions for accessing a cache, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#restrictions-for-accessing-a-cache // TODO: Force deleting cache entries, see https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#force-deleting-cache-entries package artifactcache
pkg/artifactcache/handler.go+21 −11 modified@@ -1,6 +1,8 @@ package artifactcache import ( + "crypto/rand" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -24,7 +26,7 @@ import ( ) const ( - urlBase = "/_apis/artifactcache" + apiPath = "/_apis/artifactcache" ) type Handler struct { @@ -40,6 +42,7 @@ type Handler struct { outboundIP string customExternalURL string + token string } func StartHandler(dir, customExternalURL string, outboundIP string, port uint16, logger logrus.FieldLogger) (*Handler, error) { @@ -84,19 +87,26 @@ func StartHandler(dir, customExternalURL string, outboundIP string, port uint16, h.outboundIP = ip.String() } + tokenBytes := make([]byte, 16) + if _, err := rand.Read(tokenBytes); err != nil { + return nil, fmt.Errorf("generate auth token: %w", err) + } + h.token = hex.EncodeToString(tokenBytes) + router := httprouter.New() - router.GET(urlBase+"/cache", h.middleware(h.find)) - router.POST(urlBase+"/caches", h.middleware(h.reserve)) - router.PATCH(urlBase+"/caches/:id", h.middleware(h.upload)) - router.POST(urlBase+"/caches/:id", h.middleware(h.commit)) - router.GET(urlBase+"/artifacts/:id", h.middleware(h.get)) - router.POST(urlBase+"/clean", h.middleware(h.clean)) + base := "/" + h.token + apiPath + router.GET(base+"/cache", h.middleware(h.find)) + router.POST(base+"/caches", h.middleware(h.reserve)) + router.PATCH(base+"/caches/:id", h.middleware(h.upload)) + router.POST(base+"/caches/:id", h.middleware(h.commit)) + router.GET(base+"/artifacts/:id", h.middleware(h.get)) + router.POST(base+"/clean", h.middleware(h.clean)) h.router = router h.gcCache() - listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) // listen on all interfaces + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", h.outboundIP, port)) if err != nil { return nil, err } @@ -122,9 +132,9 @@ func (h *Handler) GetActualPort() int { func (h *Handler) ExternalURL() string { if h.customExternalURL != "" { - return h.customExternalURL + return h.customExternalURL + "/" + h.token } - return fmt.Sprintf("http://%s:%d", h.outboundIP, h.GetActualPort()) + return fmt.Sprintf("http://%s:%d/%s", h.outboundIP, h.GetActualPort(), h.token) } func (h *Handler) Close() error { @@ -200,7 +210,7 @@ func (h *Handler) find(w http.ResponseWriter, r *http.Request, _ httprouter.Para } h.responseJSON(w, r, 200, map[string]any{ "result": "hit", - "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), urlBase, cache.ID), + "archiveLocation": fmt.Sprintf("%s%s/artifacts/%d", h.ExternalURL(), apiPath, cache.ID), "cacheKey": cache.Key, }) }
pkg/artifactcache/handler_test.go+35 −5 modified@@ -23,7 +23,7 @@ func TestHandler(t *testing.T) { handler, err := StartHandler(dir, "", "", 0, nil) require.NoError(t, err) - base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase) + base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath) defer func() { t.Run("inpect db", func(t *testing.T) { @@ -589,7 +589,7 @@ func uploadCacheNormally(t *testing.T, base, key, version string, content []byte func TestHandler_CustomExternalURL(t *testing.T) { dir := filepath.Join(t.TempDir(), "artifactcache") - handler, err := StartHandler(dir, "", "", 0, nil) + handler, err := StartHandler(dir, "", "127.0.0.1", 0, nil) require.NoError(t, err) defer func() { @@ -598,17 +598,17 @@ func TestHandler_CustomExternalURL(t *testing.T) { handler.customExternalURL = fmt.Sprintf("http://%s:%d", "127.0.0.1", handler.GetActualPort()) - assert.Equal(t, fmt.Sprintf("http://%s:%d", "127.0.0.1", handler.GetActualPort()), handler.ExternalURL()) + assert.Equal(t, fmt.Sprintf("http://%s:%d/%s", "127.0.0.1", handler.GetActualPort(), handler.token), handler.ExternalURL()) - base := fmt.Sprintf("%s%s", handler.ExternalURL(), urlBase) + base := fmt.Sprintf("%s%s", handler.ExternalURL(), apiPath) t.Run("advertise url set wrong", func(t *testing.T) { original := handler.customExternalURL defer func() { handler.customExternalURL = original }() handler.customExternalURL = "http://127.0.0.999:1234" - assert.Equal(t, "http://127.0.0.999:1234", handler.ExternalURL()) + assert.Equal(t, "http://127.0.0.999:1234/"+handler.token, handler.ExternalURL()) }) t.Run("reserve and upload", func(t *testing.T) { @@ -729,3 +729,33 @@ func TestHandler_gcCache(t *testing.T) { } require.NoError(t, db.Close()) } + +func TestHandler_UnauthorizedAccess(t *testing.T) { + dir := filepath.Join(t.TempDir(), "artifactcache") + handler, err := StartHandler(dir, "", "", 0, nil) + require.NoError(t, err) + defer handler.Close() + + // Try accessing without the token prefix — should get 404 + noTokenBase := fmt.Sprintf("http://%s:%d%s", handler.outboundIP, handler.GetActualPort(), apiPath) + + resp, err := http.Get(fmt.Sprintf("%s/cache?keys=test&version=abc", noTokenBase)) + require.NoError(t, err) + assert.Equal(t, 404, resp.StatusCode) + resp.Body.Close() + + resp, err = http.Post(fmt.Sprintf("%s/caches", noTokenBase), "application/json", strings.NewReader("{}")) + require.NoError(t, err) + assert.Equal(t, 404, resp.StatusCode) + resp.Body.Close() +} + +func TestHandler_BindAddress(t *testing.T) { + dir := filepath.Join(t.TempDir(), "artifactcache") + handler, err := StartHandler(dir, "", "127.0.0.1", 0, nil) + require.NoError(t, err) + defer handler.Close() + + addr := handler.listener.Addr().String() + assert.True(t, strings.HasPrefix(addr, "127.0.0.1:")) +}
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
6- github.com/advisories/GHSA-x34h-54cw-9825ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-34042ghsaADVISORY
- code.forgejo.org/forgejo/runner/issues/294nvdWEB
- github.com/nektos/act/commit/c28c27e141e8b54f9853de82f421ee09846751f7nvdWEB
- github.com/nektos/act/releases/tag/v0.2.86nvdWEB
- github.com/nektos/act/security/advisories/GHSA-x34h-54cw-9825nvdWEB
News mentions
0No linked articles in our index yet.