High severity8.8NVD Advisory· Published Mar 31, 2026· Updated Apr 3, 2026
CVE-2026-34040
CVE-2026-34040
Description
Moby is an open source container framework. Prior to version 29.3.1, a security vulnerability has been detected that allows attackers to bypass authorization plugins (AuthZ). This issue has been patched in version 29.3.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/moby/mobyGo | >= 0 | — |
github.com/docker/dockerGo | >= 0 | — |
github.com/moby/moby/v2Go | < 2.0.0-beta.8 | 2.0.0-beta.8 |
Affected products
1Patches
12 files changed · +81 −64
pkg/authorization/authz.go+21 −37 modified@@ -16,7 +16,7 @@ import ( "github.com/moby/moby/v2/pkg/ioutils" ) -const maxBodySize = 1048576 // 1MB +const maxBodySize = 4 * 1024 * 1024 // 4MiB // NewCtx creates new authZ context, it is used to store authorization information related to a specific docker // REST http session @@ -55,28 +55,31 @@ type Ctx struct { authReq *Request } -func isChunked(r *http.Request) bool { - // RFC 7230 specifies that content length is to be ignored if Transfer-Encoding is chunked - if strings.EqualFold(r.Header.Get("Transfer-Encoding"), "chunked") { - return true - } - for _, v := range r.TransferEncoding { - if strings.EqualFold(v, "chunked") { - return true - } - } - return false -} - // AuthZRequest authorized the request to the docker daemon using authZ plugins func (ctx *Ctx) AuthZRequest(w http.ResponseWriter, r *http.Request) error { var body []byte - if sendBody(ctx.requestURI, r.Header) && (r.ContentLength > 0 || isChunked(r)) && r.ContentLength < maxBodySize { - var err error - body, r.Body, err = drainBody(r.Body) - if err != nil { + if sendBody(ctx.requestURI, r.Header) { + // Wrap the original request body in a buffered reader so we can inspect + // the prefix without consuming bytes from the downstream reader. + // `Peek(maxBodySize + 1)` is used as a size check: + // - err == nil means at least maxBodySize+1 bytes are buffered/available, + // so the payload exceeds the plugin limit and is rejected. + // - otherwise, `peeked` contains the complete body bytes currently available + // (for short bodies this is the full payload), and reads from r.Body still + // stream the original body unchanged. + bufBody := bufio.NewReaderSize(r.Body, maxBodySize+1) + r.Body = ioutils.NewReadCloserWrapper(bufBody, r.Body.Close) + + peeked, err := bufBody.Peek(maxBodySize + 1) + if err == nil { + // Successfully peeked maxBodySize+1 bytes, so body is too large + // TODO: Allows plugin to opt in + return fmt.Errorf("request body too large for authorization plugin: size exceeds %d bytes", maxBodySize) + } else if err != io.EOF { return err } + + body = peeked } var h bytes.Buffer @@ -142,25 +145,6 @@ func (ctx *Ctx) AuthZResponse(rm ResponseModifier, r *http.Request) error { return nil } -// drainBody dump the body (if its length is less than 1MB) without modifying the request state -func drainBody(body io.ReadCloser) ([]byte, io.ReadCloser, error) { - bufReader := bufio.NewReaderSize(body, maxBodySize) - newBody := ioutils.NewReadCloserWrapper(bufReader, func() error { return body.Close() }) - - data, err := bufReader.Peek(maxBodySize) - // Body size exceeds max body size - if err == nil { - log.G(context.TODO()).Warnf("Request body is larger than: '%d' skipping body", maxBodySize) - return nil, newBody, nil - } - // Body size is less than maximum size - if err == io.EOF { - return data, newBody, nil - } - // Unknown error - return nil, newBody, err -} - func isAuthEndpoint(urlPath string) (bool, error) { // eg www.test.com/v1.24/auth/optional?optional1=something&optional2=something (version optional) matched, err := regexp.MatchString(`^[^\/]*\/(v\d[\d\.]*\/)?auth.*`, urlPath)
pkg/authorization/authz_unix_test.go+60 −27 modified@@ -140,36 +140,69 @@ func TestResponseModifier(t *testing.T) { } } -func TestDrainBody(t *testing.T) { - tests := []struct { - length int // length is the message length send to drainBody - expectedBodyLength int // expectedBodyLength is the expected body length after drainBody is called - }{ - {10, 10}, // Small message size - {maxBodySize - 1, maxBodySize - 1}, // Max message size - {maxBodySize * 2, 0}, // Large message size (skip copying body) +type recordingPlugin struct { + recordedRequest Request +} + +func (p *recordingPlugin) Name() string { return "recording-plugin" } + +func (p *recordingPlugin) AuthZRequest(authReq *Request) (*Response, error) { + p.recordedRequest = *authReq + p.recordedRequest.RequestBody = bytes.Clone(authReq.RequestBody) + return &Response{Allow: true}, nil +} + +func (p *recordingPlugin) AuthZResponse(_ *Request) (*Response, error) { + return &Response{Allow: true}, nil +} + +func TestAuthZRequestBodyWithinLimit(t *testing.T) { + payload := strings.Repeat("a", maxBodySize) + plugin := &recordingPlugin{} + ctx := NewCtx([]Plugin{plugin}, "user", "tls", http.MethodPost, "/containers/create") + req := httptest.NewRequest(http.MethodPost, "http://example.com/containers/create", strings.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + + if err := ctx.AuthZRequest(httptest.NewRecorder(), req); err != nil { + t.Fatalf("AuthZRequest failed: %v", err) } - for _, test := range tests { - msg := strings.Repeat("a", test.length) - body, closer, err := drainBody(io.NopCloser(bytes.NewReader([]byte(msg)))) - if err != nil { - t.Fatal(err) - } - if len(body) != test.expectedBodyLength { - t.Fatalf("Body must be copied, actual length: '%d'", len(body)) - } - if closer == nil { - t.Fatal("Closer must not be nil") - } - modified, err := io.ReadAll(closer) - if err != nil { - t.Fatalf("Error must not be nil: '%v'", err) - } - if len(modified) != len(msg) { - t.Fatalf("Result should not be truncated. Original length: '%d', new length: '%d'", len(msg), len(modified)) - } + if string(plugin.recordedRequest.RequestBody) != payload { + t.Fatalf("expected full request body to be sent to plugin, got length %d, expected %d", len(plugin.recordedRequest.RequestBody), len(payload)) + } + + remaining, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("failed to read request body after authz: %v", err) + } + if string(remaining) != payload { + t.Fatalf("request body should be preserved for downstream readers") + } +} + +func TestAuthZRequestBodyOverLimit(t *testing.T) { + payload := strings.Repeat("a", maxBodySize+1) + plugin := &recordingPlugin{} + ctx := NewCtx([]Plugin{plugin}, "user", "tls", http.MethodPost, "/containers/create") + + req := httptest.NewRequest(http.MethodPost, "http://example.com/containers/create", strings.NewReader(payload)) + req.Header.Set("Content-Type", "application/json") + + err := ctx.AuthZRequest(httptest.NewRecorder(), req) + if err == nil { + t.Fatal("expected AuthZRequest to reject body over max size") + } + if !strings.Contains(err.Error(), "request body too large for authorization plugin") { + t.Fatalf("unexpected error: %v", err) + } + + remaining, readErr := io.ReadAll(req.Body) + if readErr != nil { + t.Fatalf("failed to read request body after authz error: %v", readErr) + } + if string(remaining) != payload { + t.Fatalf("request body should still be preserved after over-limit check") } }
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
7- github.com/advisories/GHSA-x744-4wpc-v9h2ghsaADVISORY
- github.com/moby/moby/security/advisories/GHSA-x744-4wpc-v9h2nvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-34040ghsaADVISORY
- docs.docker.com/engine/extend/plugins_authorizationghsaWEB
- github.com/moby/moby/commit/e89edb19ad7de0407a5d31e3111cb01aa10b5a38ghsaWEB
- github.com/moby/moby/releases/tag/docker-v29.3.1nvdRelease NotesWEB
- github.com/moby/moby/security/advisories/GHSA-v23v-6jw2-98fqghsaWEB
News mentions
0No linked articles in our index yet.