Nov 10, 2026·10 min read·0 visits
compose-lint 0.31.0 fails to sanitize CLI argument parsing errors written to stderr by Python's argparse, allowing attackers to inject CI workflow commands via maliciously named file paths in pull requests when invoked without the '--' separator. Fixed in version 0.32.0.
An improper output neutralization vulnerability (CWE-117 / CWE-150) exists in compose-lint version 0.31.0. When compose-lint is invoked on untrusted file paths without a preceding '--' option delimiter, Python's standard argparse module rejects malformed or dash-prefixed paths and writes them directly to standard error without passing through the application's output sanitizer. An attacker submitting a pull request containing crafted directory or file names with carriage returns or Unicode whitespace can inject arbitrary CI workflow commands into GitHub Actions or Azure Pipelines job logs.
compose-lint is a static analysis security linter designed to identify misconfigurations in Docker Compose files prior to deployment. It is commonly deployed within Continuous Integration (CI) pipelines, such as GitHub Actions and Azure Pipelines, or executed via pre-commit hooks to validate untrusted code changes submitted in pull requests. Because CI runners parse standard output (stdout) and standard error (stderr) streams for inline workflow control sequences, tools operating on untrusted repository contents must neutralize control characters and command syntax before emitting diagnostic messages.
In compose-lint version 0.31.0, an incomplete output sanitization implementation left error messages generated by the command-line interface (CLI) argument parser unescaped. While version 0.31.0 introduced an output sanitizer (src/compose_lint/_output.py) to neutralize workflow commands embedded inside Docker Compose file contents, this sanitization only applied to text routed through the internal emit() function and structured report formatters. Standard error output generated directly by Python's argparse.ArgumentParser bypassed this sanitizer.
Classified under CWE-117 (Improper Output Neutralization for Logs) and CWE-150 (Improper Neutralization of Escape, Meta, or Control Sequences), this flaw allows untrusted file paths to trigger CI runner directives. When a CI pipeline passes changed file paths to compose-lint without an explicit -- option-terminating delimiter, paths beginning with dashes (- or --) are interpreted by argparse as CLI flags. When argparse rejects these unrecognized flags, it writes the raw, unsanitized input string directly to sys.stderr.
In addition to the argparse bypass, the version 0.31.0 sanitizer exhibited three secondary filter evasion flaws: incomplete handling of Unicode whitespace prefixes preceding GitHub Actions :: command tokens, failure to neutralize Azure Pipelines ##vso[ command tokens, and unescaped ##[ sequences within serialized JSON and SARIF output streams.
The root cause of CVE-2026-125123 lies in the interaction between Python's standard argparse error handling, line-ending normalization, and the stream parsing mechanics of CI runner processes. Both GitHub Actions and Azure Pipelines runners monitor step execution streams (stdout and stderr) for line-anchored or inline command tokens, such as ::notice::, ::warning::, ::error::, ##[command], and ##vso[task.setvariable]. If an attacker can position these tokens at the beginning of a parsed line—or inline for ##vso[—the runner executes the corresponding directive.
In compose-lint 0.31.0, the CLI entrypoint instantiated a standard argparse.ArgumentParser object. When ArgumentParser.parse_args() encounters an unrecognized option string (such as a directory named --malicious), it invokes self.error(), which calls self.print_usage(sys.stderr) and self.exit(2, message). The ArgumentParser._print_message() method writes the error string—including the verbatim rejected argument—directly to sys.stderr.write(), completely bypassing compose-lint's _output.py module.
To position a GitHub Actions ::command:: token at the start of a line inside an argparse error message (which normally prefixes output with compose-lint: error: unrecognized arguments: ), an attacker can embed a lone carriage return (\r) or newline (\n) within the file path or option value. Because the .NET runtime underlying the GitHub Actions runner treats a standalone carriage return (\r) as a valid line break when reading process streams, everything immediately following \r is evaluated as the beginning of a new line.
Furthermore, even for output that passed through _output.py in version 0.31.0, the sanitization regular expressions did not match the full parsing behavior of the CI runner. Specifically, the GitHub Actions runner invokes .NET's String.TrimStart() before checking for :: workflow commands, which strips all Unicode characters categorized as whitespace (such as non-breaking space \xa0, ideographic space \u3000, and em-space \u2003). In contrast, compose-lint 0.31.0 used the regular expression ^([ \t]*):(?=:) to detect line-leading colons, matching only ASCII spaces and horizontal tabs. Consequently, prefixing ::warning:: with \xa0 bypassed the regex while still being trimmed and executed by the runner.
The remediation in commit 542e63126f795564a65c63887ea0dc558a500688 addresses both the argparse output bypass in src/compose_lint/cli.py and the incomplete token neutralization in src/compose_lint/_output.py.
First, to ensure that all CLI parser output passes through the sanitization pipeline, compose-lint replaced direct usage of argparse.ArgumentParser with a custom subclass _Parser in src/compose_lint/cli.py. This subclass overrides print_usage, print_help, and exit to route all messages through emit_block() and sanitize():
# src/compose_lint/cli.py
class _Parser(argparse.ArgumentParser):
"""argparse with its own output routed through the sanitizer."""
def print_usage(self, file: SupportsWrite[str] | None = None) -> None:
# Route usage output through the custom _emit handler
self._emit(self.format_usage(), file)
def print_help(self, file: SupportsWrite[str] | None = None) -> None:
# Route help output through the custom _emit handler
self._emit(self.format_help(), file)
def exit(self, status: int = 0, message: str | None = None) -> NoReturn:
if message:
# Intercept error messages (whichnormally go straight to sys.stderr)
# and pass them through emit_block() for sanitization
emit_block(message)
sys.exit(status)
@staticmethod
def _emit(text: str, file: SupportsWrite[str] | None) -> None:
if file is sys.stderr:
emit_block(text)
else:
(sys.stdout if file is None else file).write(sanitize(text))Second, the patch updates src/compose_lint/_output.py to close the regular expression discrepancies regarding Unicode whitespace, Azure Pipelines ##vso[ tokens, and serialized JSON/SARIF output:
# src/compose_lint/_output.py
# Before (v0.31.0):
# _LEGACY_COMMAND = re.compile(r"##\[")
# _LINE_COMMAND = re.compile(r"^([ \t]*):(?=:)", re.MULTILINE)
# After (v0.32.0):
# Match both legacy GitHub Actions '##[' and Azure Pipelines '##vso[' case-insensitively
_LEGACY_COMMAND = re.compile(r"##(vso)?\[", re.IGNORECASE)
# Match any non-newline whitespace character (including Unicode whitespace like \xa0)
# at the start of a line before '::'
_LINE_COMMAND = re.compile(r"^([^\S\n]*):(?=:)", re.MULTILINE)
def _defuse_commands(text: str) -> str:
# Replace '[' with '\u005b' and ':' with '\u003a' to break runner token parsing
text = _LEGACY_COMMAND.sub(lambda m: "##" + (m.group(1) or "") + "\\u005b", text)
return _LINE_COMMAND.sub("\\1\\\\u003a", text)
def defuse_json(document: str) -> str:
"""Break every CI command opener inside serialized JSON, keeping it valid."""
# In JSON strings, replacing '[' with '\u005b' produces an identical decoded string
# for JSON parsers while preventing raw stdout stream scanners from matching '##['
return _LEGACY_COMMAND.sub(
lambda m: "##" + (m.group(1) or "") + "\\u005b", document
)> [!NOTE]
> The defuse_json() function leverages JSON's native Unicode escape syntax (\u005b for [). Because json.dumps() does not escape [ by default, raw ##[ or ##vso[ sequences inside JSON fields on stdout would otherwise be interpreted by the CI runner before any downstream JSON consumer parsed the document. Replacing [ with \u005b inside the serialized JSON string neutralizes the token in the raw character stream while preserving the exact semantic value when parsed by a compliant JSON or SARIF reader.
An assessment of the fix in commit 542e63126f795564a65c63887ea0dc558a500688 indicates that the remediation is complete. Because argparse propagates the parent parser's class to subparsers automatically (or via explicit parser_class), all CLI argument validation failures now pass through emit_block(), which normalizes control characters (including \r) and defuses command prefixes prior to writing to sys.stderr.
Exploiting CVE-2026-125123 requires that a target repository's CI workflow or custom script invokes compose-lint version 0.31.0 on untrusted file paths or arguments without preceding positional path arguments with the -- option delimiter. For example, a custom GitHub Actions workflow step that runs compose-lint $(git diff --name-only origin/main...HEAD) expands modified file names directly as CLI arguments.
An attacker with permissions to open a pull request against the repository can create a directory or file whose name begins with -- and embeds either a carriage return (\r) followed by a :: workflow command, a newline followed by Unicode whitespace (\xa0) and ::, or an inline ##[ / ##vso[ directive. Linux and macOS filesystems permit arbitrary byte sequences in file and directory names except for the null byte (\0) and path separator (/).
The regression test suite introduced in commit 542e63126f795564a65c63887ea0dc558a500688 demonstrates the exact payload structures used to trigger the vulnerability through argparse:
# Excerpt from tests/test_workflow_command_injection.py
@pytest.mark.parametrize(
"argv",
[
["--x\r::notice::cl-p4-residue-marker/compose.yml"],
["--format", "x\r::notice::cl-p4-residue-marker", "compose.yml"],
],
ids=["unrecognized-argument", "invalid-choice"],
)
def test_a_carriage_return_in_an_argument_argparse_echoes(
tmp_path: Path, argv: list[str]
) -> None:
(tmp_path / "compose.yml").write_text(
"services:\n web:\n image: nginx:1.27\n", encoding="utf-8"
)
result = subprocess.run(
[sys.executable, "-m", "compose_lint", *argv],
cwd=tmp_path,
capture_output=True,
env=cli_env(PYTHONPATH=str(REPO_ROOT / "src"), NO_COLOR="1"),
timeout=120,
)
assert result.returncode == 2When compose-lint 0.31.0 processes the argument --x\r::notice::cl-p4-residue-marker/compose.yml, argparse rejects it with exit code 2 and outputs:
usage: compose-lint [-h] [--version] ...
compose-lint: error: unrecognized arguments: --x
::notice::cl-p4-residue-marker/compose.ymlWhen the GitHub Actions runner reads stderr, the byte terminates the line compose-lint: error: unrecognized arguments: --x, placing ::notice::cl-p4-residue-marker/compose.yml at column 0 of the subsequent line. Similarly, passing --##vso[task.x]marker or --##[warning]marker/compose.yml causes the inline token to be echoed directly into the runner log stream.
The security impact of CVE-2026-125123 is constrained to CI log manipulation and workflow command injection within non-default compose-lint integrations. The vulnerability is scored at 2.3 (Low) under CVSS v4.0 (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N). The Attack Requirements (AT:P) metric reflects that exploitation depends on a specific deployment pattern where untrusted paths are passed to the CLI without --.
Importantly, two common deployment methods are unaffected even when running version 0.31.0:
compose-lint GitHub Action (action.yml) wraps execution inside a randomized ::stop-commands::<token> block and enforces argument separation, preventing the runner from interpreting workflow commands during linter execution..pre-commit-hooks.yaml definition includes -- prior to passing staged file paths, ensuring argparse treats all paths as positional arguments rather than options.In vulnerable custom CI steps or Azure Pipelines tasks, an attacker can forge build annotations (::notice::, ::warning::, ::error::), mask arbitrary strings in logs (::add-mask::), or invoke runner commands such as ##vso[task.setvariable] in Azure Pipelines to modify pipeline variables for subsequent steps within the same job. Note that in GitHub Actions, pull requests from forked repositories typically run under restricted pull_request event tokens with read-only permissions, and modern runners disable ::set-env:: and ::add-path:: workflow commands in favor of environment files, limiting the potential for direct code execution in standard GitHub Actions environments.
To remediate CVE-2026-125123, upgrade compose-lint to version 0.32.0 or later. Version 0.32.0 routes all argparse output through the output sanitizer, escapes carriage returns, expands line-prefix matching to cover all Unicode whitespace characters, and neutralizes ##[ and ##vso[ sequences across text, JSON, and SARIF output formats.
Update compose-lint across package managers, Docker containers, or pre-commit configurations as follows:
# Python / pip
pip install --upgrade "compose-lint>=0.32.0"
# Docker
docker pull composelint/compose-lint:0.32.0For repositories using pre-commit, update the rev tag in .pre-commit-config.yaml:
repos:
- repo: https://github.com/tmatens/compose-lint
rev: v0.32.0
hooks:
- id: compose-lintAs a defense-in-depth measure and workaround for environments where immediate upgrading is not feasible, always include the -- option-terminating delimiter before passing dynamic file paths or glob expansions to compose-lint in shell scripts or CI workflows:
# Vulnerable invocation when $CHANGED_FILES contains paths starting with '-'
compose-lint $CHANGED_FILES
# Hardened invocation forcing all subsequent arguments to be treated as positional paths
compose-lint -- $CHANGED_FILESAdditionally, if annotation spoofing via third-party GitHub Actions problem matchers (such as those registered by actions/setup-python, which remain active even during stop-commands blocks) is a concern in untrusted PR workflows, explicitly remove active problem matchers prior to invoking linters:
- name: Remove python problem matcher
run: echo "::remove-matcher owner=python::"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
compose-lint tmatens | = 0.31.0 | 0.32.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-125123 (GHSA-8v59-qmwv-4p6q) |
| Weakness Class | CWE-117 (Improper Output Neutralization for Logs) / CWE-150 |
| CVSS v4.0 Score | 2.3 Low (CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N) |
| Affected Component | compose-lint CLI Argument Parser (src/compose_lint/cli.py & _output.py) |
| Affected Version | 0.31.0 (Fixed in 0.32.0) |
| Attack Vector | Network (Crafted file paths in Pull Requests passed to CLI without '--') |
| Impact | CI Workflow Command Injection in GitHub Actions / Azure Pipelines logs |
| CISA KEV Status | Not Listed |
The software does not neutralize or incorrectly neutralizes output that is written to logs, allowing an attacker to forge log entries or inject malicious content into log parsers and CI runner streams.
CVE-2026-125116 (GHSA-wr5m-5jph-8m96) is a type confusion vulnerability (CWE-843) in Mongoose, the MongoDB object modeling library for Node.js. When resolving discriminator models or schemas via user-supplied discriminator keys (defaulting to `__t`), Mongoose performs direct property indexing on plain JavaScript registry objects without verifying own-property ownership. Supplying inherited `Object.prototype` property names such as `'constructor'` causes Mongoose to resolve built-in functions rather than valid Mongoose constructors, resulting in unhandled runtime exceptions, schema validation bypasses, and persistent collection query denial of service.
A flaw in Mongoose's query condition casting pipeline causes the `sanitizeFilter` configuration option to be silently ignored when defined at the connection level via `mongoose.connect()` or `mongoose.createConnection()`. Because internal query methods inspect `Connection.options` rather than `Connection.config`, applications relying on connection-scoped filter sanitization remain vulnerable to NoSQL query selector injection (CWE-943), permitting authentication bypass and unauthorized data access across Mongoose versions 6.x through 9.x.
CVE-2026-125110 is a path canonicalization mismatch vulnerability in Rack (specifically within the Rack::Static middleware component). When serving static files, Rack normalizes the requested URL path to resolve the physical file on disk, but evaluates configured path-specific header rules against an unnormalized, raw path that has only undergone URL percent-decoding. Unauthenticated remote attackers can exploit this discrepancy by injecting raw or percent-encoded dot-segments into HTTP requests, causing Rack to serve restricted static assets without their intended security headers.
CVE-2026-118776 (also tracked as GHSA-2vgq-wchc-25w7) is an improper input and integrity check validation vulnerability affecting the node-forge cryptographic library prior to version 1.4.1. In vulnerable versions, the PKCS#7 unpadding implementations for Cipher Block Chaining (CBC) and Electronic Codebook (ECB) modes only examined the final byte of the decrypted buffer and permitted padding counts up to four times the cipher block size. This defect allowed malformed ciphertexts containing zero-length padding, oversized padding values, or mismatched preceding padding bytes to pass validation, resulting in silent plaintext truncation of up to four blocks and integrity verification bypass in applications relying on unpadding success.
CVE-2026-118774 (GHSA-9wg5-mj3p-v49q) is an improper cryptographic signature verification vulnerability (CWE-347) in node-forge versions prior to 1.4.1. When verifying an X.509 subordinate certificate via certificate.verify(child) or verifyCertificateChain(), the library erroneously selects the RSA padding scheme (e.g., PKCS#1 v1.5 vs. RSASSA-PSS) and RSASSA-PSS parameters from the issuer certificate rather than from the child certificate being verified. This enables an attacker to relabel a PKCS#1 v1.5-signed certificate as an RSASSA-PSS certificate without invalidating the signature check, bypassing downstream security policies that mandate RSASSA-PSS.
CVE-2026-118670 (GHSA-xwgp-w3pj-q5h3) is a high-severity DOM-based Cross-Site Scripting (XSS) vulnerability in OHIF Viewers affecting the dicom-pdf extension module. When processing DICOM Encapsulated Document objects, the viewer extracts the untrusted MIMETypeOfEncapsulatedDocument attribute (tag 0042,0012) and uses it directly to instantiate an in-memory Blob. This Blob is then converted to a same-origin Object URL and embedded into the document via an HTML <object> element. Because browsers prioritize the resource's underlying MIME type over the <object> element's type attribute, an attacker who supplies a crafted DICOM file with a MIME type such as text/html can execute arbitrary JavaScript within the application's origin, compromising session tokens, local storage, and Patient Health Information (PHI).