---
id: "CVE-2026-125121"
page: "https://infrastack.ai/live/CVE-2026-125121"
kind: "vulnerability"
aliases: ["CVE-2026-125121", "GHSA-r7j4-crjv-h467"]
title: "compose-lint: Text in a pull request can still issue CI workflow commands through compose-lint 0.31.0's output"
severity: "medium"
cvss: 5.3
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "compose-lint", "fixed": ["0.32.0"], "affected": ">= 0.31.0, < 0.32.0", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["ci", "compose-lint", "cwe-117", "cwe-150", "docker-compose", "github-actions", "package", "pre-commit", "pypi"]
not_affected_if: ["action-sarif-file-at-root", "args-end-with-double-dash", "json-or-sarif-written-to-file", "pre-commit-not-run-in-ci"]
published_at: "2026-11-10T17:17:26+00:00"
first_seen_at: "2026-11-10T17:22:40+00:00"
updated_at: "2026-11-10T18:32:45+00:00"
sources: [{"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-125121", "first_seen_at": "2026-11-10T17:22:40+00:00"}, {"source": "ghsa", "url": "https://github.com/advisories/GHSA-r7j4-crjv-h467", "first_seen_at": "2026-11-10T18:31:48+00:00"}]
exploit_sources: []
---

# compose-lint: Text in a pull request can still issue CI workflow commands through compose-lint 0.31.0's output

Link for people: https://infrastack.ai/live/CVE-2026-125121 (this .md is for agents)

compose-lint 0.31.0 still lets text from a Compose file or a directory name become GitHub Actions workflow commands in its output, because several escape paths were missed. A pull request can use this to create annotations, hide parts of the job log, or issue other commands the runner accepts when CI lints it.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:compose-lint affected >= 0.31.0, < 0.32.0 fixed 0.32.0

## How to check

- Run `compose-lint --version` in CI or on the machine that runs it and check whether it is `0.31.0` or later but below `0.32.0`.
- Check the pinned version of the `compose-lint` package, the pre-commit hook revision, and the `composelint/compose-lint` Docker image tag in workflow files.
- Check whether the GitHub Action `tmatens/compose-lint` sets `sarif-file:` to a path in a subdirectory.

## Timeline

- 2026-11-10T17:22:40+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-125121
- 2026-11-10T18:31:48+00:00 ghsa https://github.com/advisories/GHSA-r7j4-crjv-h467

## Documents

- [advisory] Vendor advisory: github.com https://github.com/tmatens/compose-lint/security/advisories/GHSA-r7j4-crjv-h467
- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/tmatens/compose-lint/security-advisories/GHSA-r7j4-crjv-h467
- [fix] Patch: github.com https://github.com/tmatens/compose-lint/commit/542e63126f795564a65c63887ea0dc558a500688
- [fix] Release notes: github.com https://github.com/tmatens/compose-lint/releases/tag/v0.32.0
- [fix] Patch: github.com https://github.com/tmatens/compose-lint/pull/945

## From the sources

### Impact

0.31.0 fixed GHSA-6f4g-xm8v-pgv6, where text from a Compose file became GitHub Actions workflow commands in compose-lint's output. The fix escapes the command openers in everything compose-lint prints, and the 0.31.0 changelog says the report "escapes both openers (also covering Azure Pipelines)". Five paths were left open:

- **Unicode whitespace before a line-leading `::`.** The runner strips all Unicode whitespace from the start of a line before it looks for a command. compose-lint only accounted for spaces and tabs, so a no-break space or another Unicode space ahead of `::` is not escaped. It can come from a Compose value, or from a file name: the text report's file heading and per-file summary, and the `fix` summary on stderr, start with the path as given, so a directory named with a leading no-break space followed by `::` puts a command at the start of a line. This reaches those lines, the text report's fix guidance, the `fix` dry-run diff, and continuation lines of stderr diagnostics.
- **JSON and SARIF printed to stdout.** `--format json` and `--format sarif` output never passed through the sanitizer, so `##[` inside a service name or other value reaches the log intact when the output isn't redirected to a file.
- **Command-line errors.** When a repository path reaches compose-lint's arguments without a `--` before it, the argument parser prints it raw in its error message. That includes a carriage return, which the runner treats as a line break, so a command can also start a new line there.
- **Azure Pipelines.** `##vso[` logging commands were not escaped on any path, despite the changelog's Azure claim.
- **An error message in the GitHub Action (0.30.0 and 0.31.0).** When `sarif-file:` names a file in a subdirectory that can't be created, the Action prints the directory's resolved path in an error message, outside the window in which it turns command processing off. A pull request can commit a symlink on that path to a directory whose name contains a newline followed by a command. The documented setup, `sarif-file: results.sarif` at the repository root, is not affected.

A pull request can therefore create annotations, fold or hide parts of the job log, or issue other commands the runner accepts, when CI runs compose-lint over it, before anyone reviews it. The pass/fail result is not changed.

Affected setups are those that print compose-lint's output to a CI log without the GitHub Action: the `compose-lint` package run directly in a workflow, the pre-commit hook run in CI, and the Docker image. The pre-commit hook is affected in its default configuration: it passes each changed file's path to compose-lint, and a path whose directory name starts with Unicode whitespace and `::` reaches the report's file heading. The JSON/SARIF and command-line paths also need output left on stdout, or `args:` without the documented trailing `--`, which is why they alone would score lower. The GitHub Action turns command processing off around each compose-lint run, so of these paths only its own error message affects it, and only when `sarif-file:` points into a subdirectory. That setup is outside the documented one, which is why that path alone would also score lower.

Found during an internal pre-1.0 security review.

**Docker image.** `composelint/compose-lint` tag 0.31.0, the matching minor tag, and `latest` are affected.

Releases before 0.31.0 are covered by GHSA-6f4g-xm8v-pgv6, except the Action's error message, which also affects Action 0.30.0.

### Patches

Fixed in 0.32.0. The line-start escaping covers every character the runner strips before `::`, so it holds for quoted values and file names alike. JSON and SARIF on stdout escape `##[` inside strings as `\u005b`, which decodes to the same data. argparse's own messages go through the same sanitizer. `##vso[` is escaped everywhere. In the Action, the SARIF-directory error names the `sarif-file:` input as written.

### Workarounds

- Upgrade to 0.32.0 or later.
- On GitHub Actions, use the `tmatens/compose-lint` Action, or bracket the compose-lint step with `::stop-commands::<random token>` and `::<token>::`, as the Action does.
- In the Action, keep `sarif-file` at the workspace root, for example `sarif-file: results.sarif`.
- Write `--format json` and `--format sarif` output to a file rather than the log.
- End the pre-commit hook's `args:` with `--`. This closes the command-line path only; it does not stop a file name at the start of a report line, so in CI also bracket the pre-commit step with `::stop-commands::` as above.
- On Azure Pipelines, don't print compose-lint's output from runs over untrusted pull requests to the job log.

### Reproduction

On compose-lint 0.31.0 on a GitHub-hosted runner (runner 2.337.0), each of the following creates an annotation:

- a Compose file with `image: "nginx\n\u00a0::notice::X"`, linted with the text report;
- a directory named `\u00a0::notice::X` (a no-break space, then `::notice::X`) holding a `compose.yml`, linted with `compose-lint -- '<that directory>/compose.yml'`, or by the pre-commit hook in its default configuration;
- a service named `a##[warning]X`, linted with `--format json` or `--format sarif` to stdout;
- `compose-lint '--##[warning]X/compose.yml'` with no `--`;
- the Action with `sarif-file: out/reports/results.sarif`, where `out` is a committed symlink to a directory named `d\n::warning::X` that contains a regular file `reports`.

The same inputs with a plain space in place of the no-break space, or with `--` before the path, or with `sarif-file` at the repository root, create none. The same inputs on compose-lint 0.30.0 create annotations even with a plain space, which is GHSA-6f4g-xm8v-pgv6. The `composelint/compose-lint:0.31.0` image produces byte-identical output to the package.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
