
dasel v3.3.1 packaged with Melange and shipped as a minimal apko image, patched for CVE-2026-33320
This repo packages the dasel CLI (v3.3.1) as an
APK with Melange, builds a minimal container image with apko, and applies a fix for
CVE-2026-33320 while staying on the v3.3.1 codebase (a patch, not a version bump).
The result is a ~4 MB image containing only the static dasel binary — no shell, no package
manager, no libc — that runs as a non-root user.
melange/
dasel.yaml # melange recipe: fetch v3.3.1, apply the patch, build the .apk
apko/
dasel.yaml # apko recipe: assemble a minimal image from the local .apk
patches/
cve-2026-33320.patch # the CVE fix (minimal backport of upstream 282943b onto v3.3.1)
NOTES.md # what the CVE is, severity, and how the fix works
tests/
test.sh # runs the built IMAGE and asserts real behavior + the CVE fix
README.md
melange and apko on PATH.Versions used during development:
| Tool | Version |
|---|---|
| melange | 0.50.8 |
| apko | 1.2.14 |
| Docker | 29.5.2 |
All commands below are run from the repository root.
melange keygen
Produces melange.rsa (private) and melange.rsa.pub (public). Both are gitignored — keys are
regenerated by whoever reproduces the build, never committed.
melange build melange/dasel.yaml \
--source-dir patches \
--signing-key melange.rsa \
--arch x86_64 \
--runner docker
--source-dir patches makes patches/cve-2026-33320.patch available inside the build sandbox,
where the recipe's patch step applies it. Output: packages/x86_64/dasel-3.3.1-r0.apk
(signed) and packages/x86_64/APKINDEX.tar.gz.
melange test installs the freshly built .apk into a clean environment and runs the recipe's
test: block. It needs the Wolfi repo (for busybox) and our local packages/ repo (for
dasel), each with its signing key:
melange test melange/dasel.yaml dasel \
--arch x86_64 --runner docker \
--repository-append https://packages.wolfi.dev/os \
--keyring-append https://packages.wolfi.dev/os/wolfi-signing.rsa.pub \
--repository-append "$(pwd)/packages" \
--keyring-append melange.rsa.pub
Asserts: dasel version reports 3.3.1; a real JSON query; a JSON→YAML conversion; and that a
billion-laughs YAML bomb is rejected by the expansion guard (CVE-2026-33320 fixed).
apko build apko/dasel.yaml dasel:test dasel.tar --arch x86_64
docker load < dasel.tar
Output: dasel.tar (a loadable OCI image) plus an SBOM (SPDX JSON). apko appends the
architecture to the tag, so the loaded image is dasel:test-amd64.
How the image consumes the locally built package (the key constraint). apko/dasel.yaml
lists ./packages as a repository and ./melange.rsa.pub as a keyring. So apko installs the
exact dasel APK that melange build wrote into packages/x86_64/ — verified against our own
signing key — and not a prebuilt upstream package. (Run apko build from the repo root so
those relative paths resolve.) This is what connects the package build to the image build.
./tests/test.sh
Runs the image via docker run and asserts (5 checks): dasel is present and reports v3.3.1, a
nested JSON query, a JSON→YAML conversion, an array-index query, and the YAML bomb rejected. The
script exits non-zero if any check fails (so it can gate CI). Override the tag with
IMAGE=<tag> ./tests/test.sh.
CVE-2026-33320 is a denial-of-service (CWE-674, uncontrolled recursion) in dasel's YAML reader:
a "billion laughs" attack via unbounded YAML alias expansion. dasel implemented its own
UnmarshalYAML and resolved alias nodes recursively with no limit, bypassing the underlying
library's built-in protection.
We backport only the upstream fix (commit 282943b, shipped in v3.3.2) onto the v3.3.1
source as patches/cve-2026-33320.patch. It bounds expansion with a depth limit (32) and a
shared budget (1000), returning an error instead of expanding without bound. We deliberately
exclude the unrelated bug fixes that also shipped in v3.3.2, keeping the change minimal and
auditable. See patches/NOTES.md for full details.
git-checkout pins expected-commit to the v3.3.1 commit SHA, so the
build fails if the tag is ever re-pointed at different code (supply-chain safety).CGO_ENABLED=0 (set in the recipe's build environment), so the
binary has no cgo and no shared-library dependencies — which is what lets the image ship with
no libc/shell/package-manager.packages: list is just dasel; the image installs only our
locally built package (verified with our melange.rsa.pub).nonroot) for defense-in-depth.x86_64 only (the development machine's architecture).docker as Melange's runner (bubblewrap would also work on native Linux)./etc/os-release); nothing in the image
needs it. This is a deliberate minimality choice, trivially reversible by adding
wolfi-baselayout.go and busybox rather than pinned snapshots, so it assumes
Wolfi keeps serving a Go ≥ 1.25 (dasel's go.mod requirement). The dasel source is pinned by
commit; pinning the build toolchain too would make the build fully hermetic (see below).go/busybox snapshots) and a fixed build date for a
fully hermetic, bit-for-bit reproducible build (the source is already commit-pinned).aarch64) for ARM machines.melange build + melange test: package builds, patch applies cleanly (all 7 hunks — 1 in
parsing/yaml/yaml.go, 6 in parsing/yaml/yaml_reader.go), all package tests green including
the CVE bomb test.apko build + tests/test.sh: image builds (~4 MB content), all 5 image tests green.