GHSA-6WCV-RJ3C-MHV3

Vulnerability from github – Published: 2026-11-10 15:56 – Updated: 2026-11-10 15:56
VLAI
Summary
compose-lint: .env read through a symlink outside the project can expose the CI job's environment
Details

Impact

compose-lint fills in ${VAR} references from the .env file next to the Compose file. It is meant to read nothing outside the project directory, and env_file:, include: and extends: paths are checked for that. The .env read is not. If .env is a symlink, compose-lint follows it wherever it points.

A pull request can add a .env symlink to any file on the CI runner and a Compose file that references a variable from it. compose-lint substitutes the value and quotes it in finding messages, which go to the log and to JSON and SARIF output.

The worst target is /proc/self/environ, the linter's own environment. The Compose file only needs to reference the name of the first variable in it: because that file separates variables with NUL bytes rather than newlines, the substituted value is the entire rest of the environment. On a CI runner that environment holds whatever the workflow put there, including secrets. GitHub masks registered secrets in the job log, but not in uploaded JSON or SARIF artifacts or in Code Scanning alert text.

An include: entry's project_directory: has the same gap from 0.28.0: a directory symlink named there makes compose-lint read .env from wherever the link points.

Pull requests from forks normally run without secrets, and GitHub masks registered secrets in the job log, so in the common setup what can be read is runner metadata and whatever unmasked values the job's environment holds. That is why this is scored as low confidentiality impact. The impact is high in a job that runs untrusted pull requests with secrets in its environment, such as a pull_request_target workflow, or on a CI system that does not mask secrets, and also wherever the JSON or SARIF output is uploaded. The pass/fail result is not affected.

Found during an internal pre-1.0 security review.

Patches

Fixed in 0.31.0. Every .env read now applies the same containment check as env_file:. A .env that resolves outside the project is treated as missing and reported as an unread input. A project_directory: that resolves outside the project is refused, and that entry's files resolve against their own directories. A .env line containing a NUL byte is skipped.

Workarounds

  • Run with --no-env, which skips the sibling .env entirely.
  • Don't run compose-lint on untrusted pull requests in a job whose environment holds secrets.

Reproduction

project/.env -> /proc/self/environ      (symlink)
project/compose.yml:
  services:
    web:
      image: "${FIRST_VAR_NAME}"

compose-lint check compose.yml --format json quotes the environment in the CL-0004 message ("Image '...' has no tag").

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.30.0"
      },
      "package": {
        "ecosystem": "PyPI",
        "name": "compose-lint"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.22.0"
            },
            {
              "fixed": "0.31.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.30.0"
      },
      "package": {
        "ecosystem": "GitHub Actions",
        "name": "tmatens/compose-lint"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.22.0"
            },
            {
              "fixed": "0.31.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-123368"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-59"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-11-10T15:56:56Z",
    "nvd_published_at": "2026-11-09T18:17:25Z",
    "severity": "LOW"
  },
  "details": "### Impact\n\ncompose-lint fills in `${VAR}` references from the `.env` file next to the Compose file. It is meant to read nothing outside the project directory, and `env_file:`, `include:` and `extends:` paths are checked for that. The `.env` read is not. If `.env` is a symlink, compose-lint follows it wherever it points.\n\nA pull request can add a `.env` symlink to any file on the CI runner and a Compose file that references a variable from it. compose-lint substitutes the value and quotes it in finding messages, which go to the log and to JSON and SARIF output.\n\nThe worst target is `/proc/self/environ`, the linter\u0027s own environment. The Compose file only needs to reference the name of the first variable in it: because that file separates variables with NUL bytes rather than newlines, the substituted value is the entire rest of the environment. On a CI runner that environment holds whatever the workflow put there, including secrets. GitHub masks registered secrets in the job log, but not in uploaded JSON or SARIF artifacts or in Code Scanning alert text.\n\nAn `include:` entry\u0027s `project_directory:` has the same gap from 0.28.0: a directory symlink named there makes compose-lint read `.env` from wherever the link points.\n\nPull requests from forks normally run without secrets, and GitHub masks registered secrets in the job log, so in the common setup what can be read is runner metadata and whatever unmasked values the job\u0027s environment holds. That is why this is scored as low confidentiality impact. The impact is high in a job that runs untrusted pull requests with secrets in its environment, such as a `pull_request_target` workflow, or on a CI system that does not mask secrets, and also wherever the JSON or SARIF output is uploaded. The pass/fail result is not affected.\n\nFound during an internal pre-1.0 security review.\n\n### Patches\n\nFixed in 0.31.0. Every `.env` read now applies the same containment check as `env_file:`. A `.env` that resolves outside the project is treated as missing and reported as an unread input. A `project_directory:` that resolves outside the project is refused, and that entry\u0027s files resolve against their own directories. A `.env` line containing a NUL byte is skipped.\n\n### Workarounds\n\n- Run with `--no-env`, which skips the sibling `.env` entirely.\n- Don\u0027t run compose-lint on untrusted pull requests in a job whose environment holds secrets.\n\n### Reproduction\n\n```\nproject/.env -\u003e /proc/self/environ      (symlink)\nproject/compose.yml:\n  services:\n    web:\n      image: \"${FIRST_VAR_NAME}\"\n```\n\n`compose-lint check compose.yml --format json` quotes the environment in the CL-0004 message (\"Image \u0027...\u0027 has no tag\").",
  "id": "GHSA-6wcv-rj3c-mhv3",
  "modified": "2026-11-10T15:56:56Z",
  "published": "2026-11-10T15:56:56Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/tmatens/compose-lint/security/advisories/GHSA-6wcv-rj3c-mhv3"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-123368"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tmatens/compose-lint/pull/917"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tmatens/compose-lint/commit/c43204ad4dab11d54e20574a52bc4e5835246ca5"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/tmatens/compose-lint"
    },
    {
      "type": "WEB",
      "url": "https://github.com/tmatens/compose-lint/releases/tag/v0.31.0"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ],
  "summary": "compose-lint: .env read through a symlink outside the project can expose the CI job\u0027s environment"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.