Skip to content

fix(alpine): false positives for apk packages without an origin (o:) field #11322

Description

@DmitriyLewen

Description

The Alpine detector reports already fixed vulnerabilities for apk packages that have no o: (origin) field in /lib/apk/db/installed.
Such packages show up in images whose packages are not built with abuild, e.g. Docker Hardened Images.

Original report: #11311.

Reproduction

trivy image --pkg-types os dhi.io/git:2-alpine@sha256:c22bf201b598dabf2e84defb4fdddfb485cc91c215db92614b879b624795b8f2
┌─────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┐
│ Library │ Vulnerability  │ Severity │ Status │ Installed Version │ Fixed Version │
├─────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┤
│ git-lfs │ CVE-2021-38561 │ HIGH     │ fixed  │ 3.8.0-r3          │ 3.1.2-r3      │
│         │ CVE-2022-27191 │ HIGH     │ fixed  │ 3.8.0-r3          │ 3.1.2-r4      │
│         │ CVE-2025-26625 │ HIGH     │ fixed  │ 3.8.0-r3          │ 3.7.1-r0      │
└─────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┘

The installed version 3.8.0-r3 is higher than every fixed version, yet all three CVEs are reported.

The git-lfs entry in /lib/apk/db/installed (R:, Z: and similar fields omitted):

P:git-lfs
V:3.8.0-r3
A:x86_64
L:MIT
m:Docker Hardened Images <dhi@docker.com>
D:git

The other 19 packages in the image have an o: field; git-lfs does not.

Root cause

  1. The apk analyzer sets SrcName and SrcVersion only when it parses an o: line (apk.go#L88-L91).
    Without o: both fields stay empty.
  2. The Alpine detector falls back to Name when SrcName is empty (alpine.go#L89-L92, fix: In alpine use Name to fetch advisories #3079), so advisories for the package are found.
    There is no such fallback for the version: it compares utils.FormatSrcVersion(pkg), which is an empty string (alpine.go#L101).
  3. go-apk-version parses "" without an error and treats it as lower than any other version, so every advisory with a fixed version matches.
  4. The report shows the binary version (FormatVersion), which is why the installed version in the table looks higher than the fixed one.

Similar problems

The same situation — empty source fields when there is no information about the source package — exists elsewhere:

  • rpm analyzer: when SOURCERPM is (none), empty or unparsable, SrcName/SrcVersion/SrcRelease stay empty, but SrcEpoch is still taken from the binary package (rpm.go#L131-L139, rpm.go#L179).
    With a non-zero epoch, FormatSrcVersion returns a string like 1: instead of an empty one.
  • Azure Linux: advisories are looked up by pkg.SrcName with no fallback to Name (azure.go#L51), so no vulnerabilities are found for such an rpm package (false negative).
  • Root.io and Seal: the name falls back to Name, but the version comes from FormatSrcVersion (rootio.go#L85, seal.go#L110).
    checkConstraints returns false for an empty version, so fixed vulnerabilities are missed (false negative); for rpm with a non-zero epoch a string like 1: is compared.
  • RapidFort: already uses cmp.Or(utils.FormatSrcVersion(pkg), utils.FormatVersion(pkg)) (rapidfort.go#L145), but that does not kick in for rpm packages with a non-zero epoch and no SOURCERPM, because of the same 1:.

Not affected: the dpkg analyzer fills the source fields from the binary package, the same way dpkg itself does (dpkg.go#L310-L320), so Debian, Ubuntu and Echo always have a source version.
Detectors that compare the binary version (Red Hat, Amazon, Oracle, SUSE, Rocky, Alma, Photon, Wolfi, Chainguard, MinimOS) do not depend on the source version.

Challenges

  • The source fields are not only detector input.
    They end up in CycloneDX properties and SPDX sourceInfo (encode.go#L409, encode.go#L476, marshal.go#L357) and in RPC.
    For dpkg, using the binary package as the source is a rule of the format itself; for apk and rpm it is less clear-cut.
    apk has no separate source version at all (subpackages share the main package's version, and the analyzer already takes SrcVersion from V:), and apk list and apk search --origin print the package name when the origin is empty (app_list.c, app_search.c); the rest of apk-tools (replaces, index --prune-origin, apk query) treats an empty origin as absent rather than equal to the name.
    rpmdb has no source package information at all for packages without SOURCERPM.
  • utils.FormatSrcVersion is used both by detectors and by the SBOM encoder.
  • Name and version must be taken consistently: a source name with a binary version (or vice versa) produces a wrong match.
  • Checking whether the result of FormatSrcVersion is empty does not work for rpm because of SrcEpoch (see above).
  • Any change to an analyzer's output requires bumping its Version(), otherwise cached results keep serving the old data.

Before implementation

Please discuss and agree on the approach in this issue before opening a PR.

Activity

  1. added
    kind/bugCategorizes issue or PR as related to a bug.
    scan/vulnerabilityIssues relating to vulnerability scanning
    and removed
    kind/bugCategorizes issue or PR as related to a bug.
    on Sep 28, 2026
  2. added theissue type on Sep 28, 2026
  3. added
    help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.
    on Sep 28, 2026
  4. aron-intframe commented on Sep 28, 2026

    @aron-intframe

    Reproduced without DHI access: stock alpine:3.22 + apk add git-lfs, then delete the single o:git-lfs line from /lib/apk/db/installed. Nothing else differs.

    with o:git-lfs      ->  Total: 0 (HIGH: 0)
    without o:git-lfs   ->  Total: 2 (HIGH: 2)
    git-lfs CVE-2021-38561 HIGH fixed 3.6.0-r10 3.1.2-r3
    git-lfs CVE-2022-27191 HIGH fixed 3.6.0-r10 3.1.2-r4
    

    Two measurements for the approach.

    1. Origin-less apk packages already exist in official images, as a second class. Of 20 Alpine-based images I dumped, 5 carry one (python, redis, postgres, rabbitmq, memcached :alpine). They are apk add --virtual meta packages:

    .python-rundeps V:20260917.215258 A:noarch T:virtual meta package (no o:)
    

    Trivy emits it today with SrcName: null and a PURL, so a version fallback hands that timestamp to the comparator. Inert only because the .-prefixed name matches no advisory.

    2. For apk, a version fallback is a no-op whenever o: is present. The analyzer reads SrcVersion from the same V: line, so on that db FormatSrcVersion==FormatVersion 29/29, SrcName!=Name 16/29. Only the name can diverge, never the version.

    On root cause 3: NewVersion("") gives err=<nil>, Valid("")=true, but "" < 0 is false, so "" equals 0 rather than sorting below everything.

    Not checked: DHI images (auth), arm64.

    Question: fallback in alpine.go only, or in apk.go with analyzerVersion raised, and should .-prefixed virtual packages be excluded either way?

  5. akshar27 commented on Sep 28, 2026

    @akshar27

    Would like to take this on, scoped narrowly to the reported apk case — proposing the approach here first, per the note above.

    Proposed fix (apk/Alpine only)

    In alpine.go's Detect, srcName already falls back from pkg.SrcName to pkg.Name when there's no o: line. Give sourceVersion the matching fallback, from utils.FormatSrcVersion(pkg) to utils.FormatVersion(pkg):

    sourceVersion, err := version.NewVersion(cmp.Or(utils.FormatSrcVersion(pkg), utils.FormatVersion(pkg)))

    This is the same cmp.Or pattern RapidFort already uses (rapidfort.go#L144-145), applied to the one place apk still lacks it.

    Why this is safe specifically for apk, where the general challenges you listed don't apply:

    • No epoch risk. The Challenges section notes cmp.Or doesn't fully solve rpm because a non-zero SrcEpoch makes FormatSrcVersion return a non-empty "1:" even with no SOURCERPM. The apk analyzer (apk.go) never sets SrcEpoch or SrcRelease — only SrcName/SrcVersion, and only together, only from an o: line. So for apk, FormatSrcVersion(pkg) is empty iff SrcName is empty, which is exactly the condition srcName's own fallback already keys on. Name and version end up consistently binary-or-source together, never mixed.
    • No analyzer/cache change. The fix is entirely in the detector, not apk.go. SrcName/SrcVersion stay exactly as parsed (still empty without o:); this repo's SBOM output (CycloneDX/SPDX) and the RPC layer are untouched. So this doesn't hit the Version() cache-bump concern raised in the issue — that only applies to analyzer output changes, and this changes none.
    • Self-contained. No behavior change to Name, SrcName, or advisory lookup (srcName selection is already correct per the issue's root-cause step 2 and fix: In alpine use Name to fetch advisories #3079) — only the version string the resolved advisories get compared against for packages already routed to the fallback name.

    Explicitly out of scope for this PR

    Per "Similar problems", the RPM/Azure Linux/Root.io/Seal cases are real but structurally different (RPM's epoch makes the cmp.Or-is-empty check unreliable; Azure Linux has no name fallback at all — false negative, not false positive; Root.io/Seal use fallback name with a FormatSrcVersion-derived checkConstraints that reads as false on empty rather than passing wrong-but-nonempty). Each seems to want its own fix shaped around what actually breaks for it, not one shared helper — happy to look at those separately once this one lands, but proposing to keep this PR to the exact reported case only.

    Testing

    Planning to add a case to alpine_test.go reproducing this issue's exact repro (git-lfs, 3.8.0-r3 installed, no o: line, CVE with a lower fixed version) asserting it's no longer reported, alongside the existing o:-present cases to confirm no regression there.

    Let me know if this scope/approach looks right before I open a PR.

  6. DmitriyLewen commented on Sep 29, 2026

    @DmitriyLewen
    ContributorAuthor

    Question: fallback in alpine.go only, or in apk.go with analyzerVersion raised, and should .-prefixed virtual packages be excluded either way?

    I suggest discuss about vurtual packages separately.

    Hello @akshar27
    Thanks for taking on this work. The plan looks good to me.
    I've assigned this issue to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

help wantedDenotes an issue that needs help from a contributor. Must meet "help wanted" guidelines.kind/bugCategorizes issue or PR as related to a bug.scan/vulnerabilityIssues relating to vulnerability scanning

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions