VYPR
High severity7.5GHSA Advisory· Published May 9, 2026· Updated May 13, 2026

CVE-2026-42575

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.

PackageAffected versionsPatched versions
chainguard.dev/apkoGo
< 1.2.71.2.7

Affected products

1

Patches

1
a118c3d60410

apk: verify package control hash against signed APKINDEX (#2191)

https://github.com/chainguard-dev/apkoCody SoylandApr 23, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.