Skip to content

fix: reject image references that cannot be reported safely - #102

Merged
MPV merged 1 commit into
masterfrom
claude/kir-security-hardening-i0yujo-image-validation
Aug 13, 2026
Merged

fix: reject image references that cannot be reported safely#102
MPV merged 1 commit into
masterfrom
claude/kir-security-hardening-i0yujo-image-validation

Conversation

@MPV

@MPV MPV commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Validates image references before reporting them, so kir's output can be trusted by whatever consumes it. fix:, so merging bumps the version.

Problem

kir's stdout is a contract: one image per line, normally piped straight into a scanner's arguments. Every byte of it comes from a manifest kir did not write, and nothing checked those bytes. Against master, all of this printed with exit 0:

$ kir hostile.yaml
registry.k8s.io/nginx-slim:0.8
evil
second-line
nginx:1.0^[[2K^Mregistry.io/trusted:safe
--platform=linux/amd64
$ kir gaps.yaml
nginx:
{{.Values.image}}
$IMAGE
  • A line break forges an entry. One container became two lines, so anything consuming the list sees an image that doesn't exist and the real one split in half.
  • Escape sequences repaint the terminal. ^[[2K^M is erase-line plus carriage return: an operator reads registry.io/trusted:safe while the scanner is handed the nginx:1.0… value.
  • A leading dash becomes a flag. xargs syft passes --platform=linux/amd64 as an option, so a manifest author reaches into the scanner's argv.
  • Values no registry could serve — empty tag, unrendered Helm template, unexpanded shell variable — were reported as images.

Change

$ kir hostile.yaml
registry.k8s.io/nginx-slim:0.8
$ echo $?
1
error: hostile.yaml: invalid image reference "evil\nsecond-line": "invalid reference format"
error: hostile.yaml: invalid image reference "nginx:1.0\x1b[2K\rregistry.io/trusted:safe": "invalid reference format"
error: hostile.yaml: invalid image reference "--platform=linux/amd64": "invalid reference format"

A new imageref package holds the rule; cmd applies it where stdout, stderr and the exit code meet. An unreportable value gets ADR 0008's treatment — named on stderr, counted against the exit code — without discarding the images beside it that were fine.

Validation is delegated to distribution/reference, the canonical parser. kir then accepts exactly what a registry client would, so anything it admits is pullable and anything it refuses was never an image — which is what makes refusing safe.

Two deliberate limits on what's borrowed:

  • Validation only, not normalisation. kir reports what the manifest said, so nginx stays nginx rather than becoming docker.io/library/nginx.
  • The parser's message is escaped, not interpolated. It quotes the offending value back, so printing it raw would reintroduce the terminal-spoofing problem on stderr. It goes through strconv.Quote.

Why not hand-written rules

An earlier revision of this PR did exactly that — reject control characters, whitespace, a leading dash, empty — on the theory that a full grammar risks rejecting an unusual-but-legitimate reference, and that a dropped image is worse than a bad one printed. I tested that theory against the library and it didn't hold:

hand-written ParseNormalizedNamed
8 legitimate refs (digests, ports, multi-level paths, dotted tags) accept accept
5 hostile values (newline, ANSI, leading dash, spaces, empty) reject reject
nginx: / {{.Values.image}} / $IMAGE accept reject

No over-rejection on anything real, and the hand-written version had a genuine hole. The library is strictly better here.

One trap worth knowing

While comparing them I saw the library reject every digest-pinned reference with unsupported digest algorithm, and nearly concluded it over-rejects. The cause was my test harness missing import _ "crypto/sha256" — go-digest resolves sha256 only when that hash is linked in.

So imageref.go carries that blank import. To be precise about what it does and doesn't buy, all measured rather than assumed:

  • kir links crypto/sha256 today anyway, through the Kubernetes libraries, so the import changes nothing at present — digest-pinned images work without it.
  • It does make imageref self-sufficient: with it, crypto/sha256 is in the package's own dependency closure; without it, it isn't. That matters because the candidate answers to Dynamically find PodSpec in manifests #26 drop client-go, which is what currently links the hash.
  • No test here can pin it — the test binary links crypto/sha256 whatever imageref imports, so removing the import keeps the suite green. It's held in place by a comment instead, which is stated as such rather than dressed up as a guard.

Cost

Two new modules: github.com/distribution/reference v0.6.0 and github.com/opencontainers/go-digest v1.0.0 (4 go.sum lines). Neither was already in the tree — I checked go list -m all and go mod graph — so this is a genuine addition, and it cuts slightly against #84's goal of shrinking the dependency set. Both are small and canonical, but the trade is real and yours to weigh.

Tests

  • imageref: 14 rejection cases and 12 references that must keep passing, including two digest-pinned ones.
  • imageref: a test asserting the error message carries no raw \x1b, \r or \n — the parser embeds the value, so this pins the escaping.
  • cmd: drives Run end to end and asserts stdout carries only the reportable image, exit is 1, and stderr has no raw escape byte.

Hostile inputs live in Go source rather than an approvals fixture, per AGENTS.md — a checked-in .yaml would have its escapes and trailing whitespace normalised, and the golden would stop guarding anything.

Every existing golden passes unchanged, including #103's TestFailure/PartialStream. Verified the two fixes compose: a stream with both a malformed document and a bad reference reports each failure separately, exits 1, and still prints both reportable images.

Checklist

gofmt -l . empty · go vet ./... clean · go test ./... and -race green · go mod tidy no drift on the committed tree.

@MPV
MPV force-pushed the claude/kir-security-hardening-i0yujo-image-validation branch from 2e81af6 to c6b29ff Compare August 10, 2026 18:19

MPV commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto f2cc80e (post-0.4.4). The conflict was in cmd.go, where #103 had changed the same call sites; resolved by composing both rather than taking a side:

    images, err := processor.ProcessFile(filePath)
    // Not `continue`: a file that failed on one document may still have
    // yielded images from the others...
    failures += logErrors(logger, err)
    failures += printImages(stdout, logger, filePath, images)

So document-level failures (#103) and unreportable image values (this PR) both count toward the exit code, and neither discards what the other found.

Worth checking the case only the combination produces — a stream with a malformed document and a bad image reference:

$ kir both.yaml
registry.k8s.io/nginx-slim:0.8
after-the-break:1.0
error: yaml: line 8: did not find expected ',' or ']'
error: both.yaml: image reference "--platform=linux/amd64" starts with a dash

Exit 1, both failures named separately, and both reportable images survive — including the one from the document that also held the bad reference, and the one after the unparseable document.

Re-ran the full checklist after the rebase: gofmt empty, go vet clean, go test ./... and -race green — including #103's new TestFailure/PartialStream golden, which this change doesn't shift — and go mod tidy no drift.


Generated by Claude Code

@MPV
MPV force-pushed the claude/kir-security-hardening-i0yujo-image-validation branch from c6b29ff to 75640bd Compare August 13, 2026 07:21
kir's stdout is a contract — one image per line, normally piped straight into a
scanner's arguments — but every byte of it comes from a manifest kir did not
write, and nothing checked those bytes.

An image value holding a line break forged an extra entry in the list. One
holding escape sequences could make a terminal display a registry the scanner
was never given. One starting with a dash reached the scanner as an option
rather than an operand. Values no registry could serve at all — an empty tag, an
unrendered Helm template, an unexpanded shell variable — were reported as
images. All of it passed with exit 0.

Such a value is now reported on stderr and counted against the exit code, the
same shape ADR 0008 gives a malformed document, while the other images in the
document are still printed.

Validation is delegated to distribution/reference, the canonical parser, rather
than to hand-written rules: kir then accepts exactly what a registry client
would, so anything it admits is pullable and anything it refuses was never an
image. Only validation is borrowed, not normalisation — kir reports what the
manifest said, so "nginx" stays "nginx".

The parser quotes the offending value back in its own message, so that message
is escaped rather than interpolated: a reference carrying escape sequences must
not repaint the terminal it is reported on.
@MPV
MPV force-pushed the claude/kir-security-hardening-i0yujo-image-validation branch from 75640bd to e4aa27c Compare August 13, 2026 07:41
@MPV
MPV merged commit a1b7ca9 into master Aug 13, 2026
1 check passed
@MPV
MPV deleted the claude/kir-security-hardening-i0yujo-image-validation branch August 13, 2026 07:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants