JSTGTECH
← Back to blog

Malicious Docker Hub Images Are Hiding in Plain Sight

4 min read

Every other post in this series about container risk has been about what happens after a workload is already running — a kernel escape, an overprivileged default service account, an exposed API server. This one is earlier in the chain: the image itself. Docker Hub is the default, unauthenticated-by-default registry for millions of docker pull and FROM lines, and researchers have repeatedly found it hosting images that are malicious by design, not compromised after the fact. Sysdig’s 2024 scan of public Hub content found roughly a fifth of the images it sampled carrying some form of malicious payload — cryptominers dominant, but also embedded secrets, trojanized binaries, and DNS-based data exfiltration (Sysdig), and JFrog’s earlier registry sweep independently found thousands of images built purely to mine coins or open reverse shells on docker run (JFrog). None of this needs a CVE. It needs someone to trust a tag.

Root cause

Docker Hub’s trust model was built for a smaller, friendlier ecosystem. Anyone can push a public image under their own namespace, and nothing about the platform verifies that postgres-backup-tool or redis-utils does what the name implies. Two patterns show up over and over in the research. The first is straightforward typosquatting: a malicious image published under a name one character off from a popular official one — nginx- prefixed variants, misspelled base-image names — betting that a rushed docker pull or a copy-pasted docker-compose.yml won’t notice. The second, more effective pattern doesn’t rely on a typo at all: attackers publish images with plausible, professional-sounding names — backup utilities, monitoring agents, “official-looking” database tools — with real functionality bundled around a hidden payload, so the image does what it claims and runs a miner in the background. JFrog’s sweep found payloads triggered via ENTRYPOINT and embedded shell scripts that curl | bash a second-stage payload at container start, which also means static image scanning that only inspects filesystem layers for known malware signatures can miss logic that only executes at runtime. Docker Hub’s own “Official Images” and “Verified Publisher” programs exist precisely to counter this, but they cover a small slice of the registry — most pulls in the wild target unverified, community-namespace images that carry none of that scrutiny.

Blast radius

A malicious image doesn’t need a privilege-escalation bug to do damage; it already runs as whatever the container spec grants it, which in CI runners and quick docker run experiments is often more than it should be. The common outcome documented across both research sets is cryptomining — CPU-cycle theft that’s easy to dismiss as a nuisance but that also confirms the attacker had arbitrary code execution inside your environment, with network egress, the means to read any mounted volume or environment variable, and — if the image reached a CI pipeline — the same access every build step gets: source code, deployment credentials, and whatever secrets the pipeline injects. Images pulled into Kubernetes clusters inherit whatever service account and network policy the pod spec grants, so a miner today is a pivot point tomorrow if the attacker decides to escalate instead of just farming coins. And because the image is the artifact everyone in a team pulls identically, one bad tag reaching a shared base image or a widely referenced docker-compose.yml multiplies the blast radius across every environment that built from it.

Remediation

  • Pin images by digest, not tag, in anything that matters — image@sha256:... instead of image:latest. Tags are mutable; a publisher (or an attacker who compromises a publisher’s account) can repoint a tag to different content without changing the reference your manifest holds.
  • Prefer Docker Official Images and Verified Publisher images for base layers, and treat any other public-namespace image as unverified third-party code until proven otherwise — because that’s what it is.
  • Run images through a scanner that executes, not just inspects, or at minimum a static scanner (Trivy, Grype, Docker Scout) as a required CI gate before an image is allowed to build on top of or deploy from — static scanning alone won’t catch every runtime-triggered payload, but it catches the cheap, common cases for free.
  • Mirror the base images you actually depend on into a private registry you control, and build from that mirror instead of pulling upstream on every build — it turns “did the public tag change” into a question you control the answer to.
  • Restrict egress from build and runtime environments so a payload that does land can’t freely reach out to a mining pool or a C2 endpoint; default-deny network policies catch a class of attack that image scanning never will.
  • Audit who can push to any internal or shared namespace with the same rigor as a source-code repository — a registry credential is a code-execution credential for everyone who trusts that namespace.

The bigger lesson

Container images get treated like configuration — something you reference by name and move on from — when they’re really compiled, executable artifacts pulled from a registry with roughly the same trust guarantees as a random npm package before secret-scanning and lockfiles became standard practice. The fix isn’t exotic: pin by digest, source from verified publishers, scan before you build on top of anything, and mirror what you actually rely on. None of that requires new tooling most teams don’t already have — it requires applying the scrutiny already given to dependency files to the one artifact still pulled on trust.

Related posts