CVE-2026-42575
Description
apko allows users to build and publish OCI container images built from apk packages. Prior to version 1.2.7, apko verifies the signature on APKINDEX.tar.gz but never compares individually downloaded .apk packages against the checksum recorded in the signed index. The checksum is parsed and available via ChecksumString(), and the downloaded package control hash is computed, but the two values are never compared in getPackageImpl(). Mismatched packages are silently accepted. An attacker who can substitute download responses (compromised mirror, HTTP repository, poisoned CDN cache) can install arbitrary packages into built images. This issue has been patched in version 1.2.7.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
chainguard.dev/apkoGo | < 1.2.7 | 1.2.7 |
Affected products
1- Range: < 1.2.7
Patches
1a118c3d60410apk: verify package control hash against signed APKINDEX (#2191)
3 files changed · +144 −2
pkg/apk/apk/install_test.go+1 −1 modified@@ -416,7 +416,7 @@ func fakePackage(t *testing.T, pkg *Package, entries []testDirEntry) Installable return &testPackage{ pkg: pkg, file: f.Name(), - checksum: base64.StdEncoding.EncodeToString(h.Sum(nil)), + checksum: "Q1" + base64.StdEncoding.EncodeToString(h.Sum(nil)), } }
pkg/apk/apk/package_getter.go+56 −1 modified@@ -170,6 +170,11 @@ func (d *defaultPackageGetter) getPackageImpl(ctx context.Context, pkg Installab return nil, fmt.Errorf("expanding %s: %w", pkg.PackageName(), err) } + if err := verifyControlHash(pkg, exp.ControlHash); err != nil { + _ = exp.Close() + return nil, err + } + // If we don't have a cache, we're done. if d.cache == nil { return exp, nil @@ -178,6 +183,43 @@ func (d *defaultPackageGetter) getPackageImpl(ctx context.Context, pkg Installab return d.cachePackage(ctx, pkg, exp, cacheDir) } +// sha1File returns the SHA-1 of the file at path. +func sha1File(path string) ([]byte, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + h := sha1.New() //nolint:gosec // this is what apk tools is using + if _, err := io.Copy(h, f); err != nil { + return nil, err + } + return h.Sum(nil), nil +} + +// verifyControlHash compares the SHA-1 of the downloaded package's control +// section against the Q1-prefixed base64 checksum recorded in the signed +// APKINDEX (or lock file). Without this check a compromised mirror or +// poisoned cache could substitute arbitrary package contents even though +// the index itself is signature-verified. +func verifyControlHash(pkg InstallablePackage, controlHash []byte) error { + chk := pkg.ChecksumString() + if !strings.HasPrefix(chk, "Q1") { + return fmt.Errorf("package %q has unexpected checksum format: %q", pkg.PackageName(), chk) + } + expected, err := base64.StdEncoding.DecodeString(chk[2:]) + if err != nil { + return fmt.Errorf("package %q has malformed checksum %q: %w", pkg.PackageName(), chk, err) + } + if len(expected) == 0 { + return fmt.Errorf("package %q has empty checksum", pkg.PackageName()) + } + if !bytes.Equal(expected, controlHash) { + return fmt.Errorf("package %q control hash mismatch: expected %x, got %x", pkg.PackageName(), expected, controlHash) + } + return nil +} + // fetchPackage fetches a package from the network or local filesystem. func (d *defaultPackageGetter) fetchPackage(ctx context.Context, pkg FetchablePackage) (io.ReadCloser, error) { log := clog.FromContext(ctx) @@ -322,8 +364,21 @@ func (d *defaultPackageGetter) cachedPackage(ctx context.Context, pkg Installabl if err != nil { return nil, err } + + // Recompute the hash of the on-disk control file rather than trusting + // the content-addressable filename. A missed check here would let a + // tampered or corrupted cache entry be served without the verification + // that getPackageImpl applies on the fetch path. + ctlHash, err := sha1File(ctl) + if err != nil { + return nil, fmt.Errorf("hashing cached control %q: %w", ctl, err) + } + if err := verifyControlHash(pkg, ctlHash); err != nil { + return nil, fmt.Errorf("cached %q: %w", ctl, err) + } + exp.ControlFile = ctl - exp.ControlHash = checksum + exp.ControlHash = ctlHash exp.ControlSize = cf.Size() control, err := exp.ControlData()
pkg/apk/apk/package_getter_test.go+87 −0 modified@@ -2,6 +2,7 @@ package apk import ( "context" + "encoding/base64" "fmt" "io" "net/http" @@ -263,3 +264,89 @@ func TestAuth_bad(t *testing.T) { require.Error(t, err, "unable to expand package") require.True(t, called, "did not make request") } + +// TestGetPackage_ChecksumMismatch confirms that a package served by a repository +// that does not match the checksum recorded in the (signed) APKINDEX is rejected +// rather than silently installed. This guards against compromised mirrors or +// poisoned caches substituting package contents. +func TestGetPackage_ChecksumMismatch(t *testing.T) { + tampered := testPkg + // Flip one byte of the recorded checksum so the downloaded content's + // real control-section SHA-1 will not match. + tampered.Checksum = append([]byte(nil), testPkg.Checksum...) + tampered.Checksum[0] ^= 0xff + + repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)} + repoWithIndex := repo.WithIndex(&APKIndex{Packages: []*Package{&tampered}}) + pkg := NewRepositoryPackage(&tampered, repoWithIndex) + ctx := context.Background() + + tmpDir := t.TempDir() + httpClient := &http.Client{Transport: &testLocalTransport{root: testPrimaryPkgDir, basenameOnly: true}} + a := newDefaultPackageGetter(httpClient, &cache{ + dir: tmpDir, + offline: false, + shared: NewCache(false), + }, auth.DefaultAuthenticators) + + _, err := a.GetPackage(ctx, pkg) + require.Error(t, err, "expected checksum mismatch to be detected") + require.Contains(t, err.Error(), "control hash mismatch") +} + +// TestCachedPackage_TamperedControl confirms that a cache entry whose +// on-disk control file no longer matches its content-addressable filename +// is rejected rather than served. This protects against cache corruption +// or tampering after an entry was originally written. +func TestCachedPackage_TamperedControl(t *testing.T) { + repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)} + repoWithIndex := repo.WithIndex(&APKIndex{Packages: []*Package{&testPkg}}) + pkg := NewRepositoryPackage(&testPkg, repoWithIndex) + ctx := context.Background() + + tmpDir := t.TempDir() + httpClient := &http.Client{Transport: &testLocalTransport{root: testPrimaryPkgDir, basenameOnly: true}} + a := newDefaultPackageGetter(httpClient, &cache{ + dir: tmpDir, + offline: false, + shared: NewCache(false), + }, auth.DefaultAuthenticators) + + // Populate the cache. + exp, err := a.GetPackage(ctx, pkg) + require.NoError(t, err, "populating cache") + ctlPath := exp.ControlFile + require.FileExists(t, ctlPath) + + cacheDir := filepath.Dir(ctlPath) + + // Tamper with the cached control file. Overwrite with different bytes + // so its SHA-1 no longer matches the content-addressable filename. + require.NoError(t, os.WriteFile(ctlPath, []byte("tampered"), 0o644)) + + _, err = a.cachedPackage(ctx, pkg, cacheDir) + require.Error(t, err, "expected tampered cache entry to be rejected") + require.Contains(t, err.Error(), "control hash mismatch") +} + +func TestVerifyControlHash(t *testing.T) { + want := make([]byte, 20) + for i := range want { + want[i] = byte(i) + } + pkg := &testPackage{ + pkg: &Package{Name: "example"}, + checksum: "Q1" + base64.StdEncoding.EncodeToString(want), + } + + require.NoError(t, verifyControlHash(pkg, want)) + + bad := append([]byte(nil), want...) + bad[0] ^= 0xff + require.Error(t, verifyControlHash(pkg, bad)) + + require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: "x"}, checksum: ""}, want)) + require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: "x"}, checksum: "Q1"}, want)) + require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: "x"}, checksum: "Q1!!!"}, want)) + require.Error(t, verifyControlHash(&testPackage{pkg: &Package{Name: "x"}, checksum: "raw-no-prefix"}, want)) +}
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
5- github.com/advisories/GHSA-hcwr-pq9g-rq3mghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-42575ghsaADVISORY
- github.com/chainguard-dev/apko/commit/a118c3d604107532b5525bd4bee2fb369a6228aanvdWEB
- github.com/chainguard-dev/apko/releases/tag/v1.2.7nvdWEB
- github.com/chainguard-dev/apko/security/advisories/GHSA-hcwr-pq9g-rq3mnvdWEB
News mentions
0No linked articles in our index yet.