fix: request registry tokens with the target repository scope (#1478)

* fix: request registry tokens with the target repository scope

The image-update detector authenticated to registries by reusing the scope
echoed in the registry's GET /v2/ ping. That ping carries no repository
context, and ghcr.io answers it with a placeholder scope
(repository:user/image:pull), so the token was requested for the wrong
repository and rejected. Every ghcr.io-backed image (including lscr.io, which
delegates auth to ghcr.io) then failed its manifest lookup and was reported as
"Registry unreachable", while Docker Hub and quay.io kept working. Always
request a pull scope for the repository being checked rather than the echoed
placeholder.

Also report the actual failure cause: getRemoteDigestResult now distinguishes
an authentication failure, a rate limit (with retry-after), a missing image, a
registry error, and a genuinely unreachable registry, instead of collapsing
every failure into "Registry unreachable". getRemoteDigest stays a
digest-or-null wrapper so the update-preview path is unchanged, and
listRegistryTags shares the same token path so it now resolves on
ghcr.io/lscr.io too.

* fix: neutralize control characters in the registry digest error log

The error-path console.error in getRemoteDigestResult interpolated the image
ref and the caught error message, both of which originate from compose-authored
input. Route them through sanitizeForLog so a crafted image string or upstream
error text cannot forge multi-line log entries (log injection). The returned
reason and the digest logic are unchanged.
This commit is contained in:
Anso
2026-06-26 21:06:40 -04:00
committed by GitHub
parent 628400ac19
commit 2911ccfe2b
5 changed files with 231 additions and 28 deletions
+5 -4
View File
@@ -8,7 +8,7 @@ import { RegistryService } from './RegistryService';
import { NodeRegistry } from './NodeRegistry';
import { NotificationService } from './NotificationService';
import { sanitizeNotificationMessage } from '../utils/notificationMessage';
import { parseImageRef, getRemoteDigest, repoDigestMatchesRef } from './registry-api';
import { parseImageRef, getRemoteDigestResult, repoDigestMatchesRef } from './registry-api';
import { isDebugEnabled } from '../utils/debug';
import { getErrorMessage } from '../utils/errors';
import { sanitizeForLog } from '../utils/safeLog';
@@ -705,10 +705,11 @@ export class ImageUpdateService {
return { hasUpdate: false, error: `Could not resolve a local registry digest for "${imageRef}"` };
}
const remoteDigest = await getRemoteDigest(parsed.registry, parsed.repo, parsed.tag, credentials);
if (!remoteDigest) {
return { hasUpdate: false, error: `Registry unreachable for ${parsed.registry}/${parsed.repo}:${parsed.tag}` };
const remote = await getRemoteDigestResult(parsed.registry, parsed.repo, parsed.tag, credentials);
if (!remote.ok) {
return { hasUpdate: false, error: remote.reason };
}
const remoteDigest = remote.digest;
const hasUpdate = localDigest !== remoteDigest;
console.log(