Skip to content

fix(ownership): authorize the query tag on Docker-compatible image push - #521

Open
MgSrdEer wants to merge 1 commit into
CodesWhat:mainfrom
MgSrdEer:fix/ownership-image-push-tag
Open

MgSrdEer wants to merge 1 commit into
CodesWhat:mainfrom
MgSrdEer:fix/ownership-image-push-tag

Conversation

@MgSrdEer

Copy link
Copy Markdown

Problem

POST /images/{name}/push names its subject in two pieces: the repository in the path and the tag in ?tag=. imageIdentifier strips the /push suffix and handed the bare repository to the ownership inspect, which resolves the daemon's default tag (typically :latest). Two failures followed:

  1. False denial. A proxy-scoped build pushed as {name}:{tag} was denied with owner policy could not resolve image whenever {name}:latest happened to be absent. That is the standard docker push registry/repo:tag CLI spelling (the CLI sends the tag in the query, never in the path), so every tagged push against an image without a local :latest tag failed.
  2. Privilege over-grant. When {name}:latest WAS present, the push was authorized on tag-ref equality: a caller owning only :latest could push {name}:{anything}.

Fix

The mutation pass captures the query tag (ownershipRequestReferences.imagePushTag) and the authorization pass qualifies the identifier with it (appendImagePushTag), so the inspect resolves exactly the local image the daemon will push.

Push forms whose effective reference one image inspect cannot enumerate are refused instead of guessed at, via the denyReason path commit already uses for query-named resources:

  • No tag at all: distribution.Push pushes every local reference of the repository (daemon/internal/distribution/push.go: "If no tag is provided, all tags are pushed"), an effect one image inspect cannot enumerate — the same shape imageEffectDenial refuses per-image exports and deletes for. The docker CLI never sends this form (it resolves a name-only reference to ?tag=latest), so it is reachable only by direct API clients.
  • A repeated or case-variant tag spelling: dockerd reads the first value and Podman's compat handler the last (filter.FoldedScalarQueryValue is the existing helper for this disagreement).

The retag route POST /images/{name}/tag keeps its bare-path authorization: the resource it mutates is the source image the path names, and docker tag src dst spells the full source reference into the path. Podman's native POST /libpod/images/{name}/push carries the full reference in the path and is unaffected.

Verification

Unit tests in image_push_test.go (fake-inspector middleware tests, route classification, query parsing), plus an end-to-end run against dockerd 26.1.3 with a real registry:

scenario result
push owned {repo}:{tag}, no local :latest ✅ allowed (failed before the fix)
bare API push, no tag query ✅ denied "without an explicit tag"
docker push repo (CLI resolves to ?tag=latest) ✅ 404 fail-closed when :latest absent, allowed when owned
repeated ?tag=a&tag=b ✅ 403 "ambiguous tag parameter"
push a foreign-owned tag ✅ 403 "denied access to image"
retag route (POST /images/{name}/tag) ✅ unchanged bare-path authorization

go test ./... passes (41 packages).

POST /images/{name}/push names its subject in two pieces: the repository
in the path and the tag in ?tag=. imageIdentifier strips the "/push"
suffix and handed the bare repository to the ownership inspect, which
resolves the daemon's default tag (typically :latest). Two failures
followed:

- A proxy-scoped build pushed as {name}:{tag} was denied with "owner
  policy could not resolve image" whenever {name}:latest happened to be
  absent. That is the standard `docker push registry/repo:tag` CLI
  spelling (the CLI sends the tag in the query, never in the path), so
  every tagged push against an image without a local :latest tag failed.
- When {name}:latest WAS present, the push was authorized on tag-ref
  equality: a caller owning only :latest could push {name}:{anything}.

The mutation pass now captures the query tag
(ownershipRequestReferences.imagePushTag) and the authorization pass
qualifies the identifier with it, so the inspect resolves exactly the
local image the daemon will push.

Push forms whose effective reference one image inspect cannot enumerate
are refused instead of guessed at, via the denyReason path commit
already uses for query-named resources:

- No tag at all: distribution.Push pushes every local reference of the
  repository (daemon/internal/distribution/push.go: "If no tag is
  provided, all tags are pushed"), an effect one image inspect cannot
  enumerate — the same shape imageEffectDenial refuses per-image exports
  and deletes for. The docker CLI never sends this form (it resolves a
  name-only reference to ?tag=latest), so it is reachable only by direct
  API clients.
- A repeated or case-variant tag spelling: dockerd reads the first value
  and Podman's compat handler the last (filter.FoldedScalarQueryValue is
  the existing helper for this disagreement).

The retag route POST /images/{name}/tag keeps its bare-path
authorization: the resource it mutates is the source image the path
names, and `docker tag src dst` spells the full source reference into
the path. Podman's native POST /libpod/images/{name}/push carries the
full reference in the path and is unaffected.

Verified end to end against dockerd 26.1.3: a tagged push of an owned
image without any local :latest succeeds; the bare and repeated-tag
forms get their new denials; a foreign-owned tag is refused with the
ordinary cross-owner denial.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

@MgSrdEer is attempting to deploy a commit to the CodesWhat Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: b8e2e95f-0faa-4158-a36c-f616e3e5f11a


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant