Tags are not immutable

ID

tag_protection

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Family

SCM

Tags

ASVS50:v15.1.1, cicd-sec-09, cicd-security, security, source-code, spvs10-v3.3.18, supply-chain, tag-protection

Description

Are the repository’s tags protected against being moved or deleted?

A release tag is a trust anchor: consumers pin a GitHub Action or a download to a tag (for example uses: org/action@v1) and assume the tag keeps pointing at the reviewed commit. Tags are mutable by default — a tag can be re-pointed to a different commit or deleted and recreated. The check fetches the repository’s actual tags and reports the ones not covered by an active tag-protection rule that restricts both tag update and tag deletion.

The check is only raised for repositories that actually publish tags — a repository with no tags has nothing to protect. Coverage is evaluated per tag against the rule’s ref/name scope: a rule scoped to v* protects v1.2.3 but leaves release-2024 mutable, so that tag is reported. A GitHub ruleset that only enforces a naming pattern or signatures — without restricting update and deletion — does not make tags immutable and will still be reported.

Platform support

The tag-protection primitive is platform-specific; the detector evaluates each platform against its native mechanism:

Platform Tag-protection primitive Status

GitHub / GitHub Enterprise

Ruleset with target=tag restricting update + deletion (ref-name scope matched per tag)

Supported

GitLab / GitLab Enterprise

Protected tags whose name pattern matches the tag

Supported

Gitea

Tag protection rules whose name pattern matches the tag

Supported

Azure DevOps

No per-tag immutability rule — tag create/delete is governed by repository security-namespace permissions, not a queryable protected-tags primitive

Not supported

Bitbucket (Cloud / Data Center)

Branch restrictions apply to branches, not tags; no protected-tags API

Not supported

Where a platform exposes no native tag-protection primitive, the detector reports nothing for that platform (it never guesses), to avoid false positives.

Impact context

On GitHub, the reported severity and message are modulated by how the repository’s tags are consumed:

  • Custom GitHub Action repository (an action.yml / action.yaml at the root) — catastrophic, reported at critical: consumers run uses: owner/repo@<tag>, so a moved tag executes attacker-controlled code in every consumer’s pipeline (the xygeni-action class of incident).

  • Tag-triggered CI/CD (on: release, or on: push with a tags: filter) — a re-pointed tag runs the pipeline against tainted code.

  • Published releases — release artifacts are resolved by tag, so consumers downloading by tag receive tampered content.

Security

Tag immutability is the control that stops tag-poisoning supply-chain attacks. If an attacker who has gained write access (or a compromised token/bot) can move a widely-consumed release tag to attacker-controlled code, every downstream pipeline that pins to that tag will silently execute the malicious version on its next run.

This is the class of weakness behind the xygeni-action tag-poisoning incident and, more broadly, the reason SHA-pinning of third-party actions is recommended (see pipeline_external_dependencies). Tag immutability protects the producer side (your published tags can’t be moved), while SHA-pinning protects the consumer side (you don’t trust someone else’s mutable tag). Both are needed.

Mitigation / Fix

GitHub — create a ruleset that targets tags and, at minimum:

  • Restrict updates — prevents an existing tag from being moved to a different commit.

  • Restrict deletions — prevents a tag from being deleted (and recreated elsewhere).

Set the ruleset enforcement to Active, and scope its target tag pattern to your release tags (for example v*). See Managing rulesets for a repository.

GitLab — add a protected tag whose name (or wildcard, e.g. v*) covers your release tags, restricting who can create/overwrite them.

Gitea — add a protected tag rule whose name pattern covers your release tags.

Configuration

To change these options you can modify SCANNER_DIR/conf/misconfigurations/tag_protection.yml. The following are the default configuration properties:

# Configuration for the tag immutability check.
properties:
  # If true, require an active tag-protection rule (GitHub ruleset target=tag / GitLab protected tags)
  # that restricts both tag update and deletion.
  requireTagImmutability: true