GHSA-4XGF-CPJX-PC3J

Vulnerability from github – Published: 2026-06-19 22:10 – Updated: 2026-06-19 22:10
VLAI
Summary
pydantic-settings: NestedSecretsSettingsSource follows symlinks outside secrets_dir, enabling local file read and bypassing secrets_dir_max_size
Details

Summary

NestedSecretsSettingsSource reads secret values from files in a configured secrets_dir. When secrets_nested_subdir=True, a directory entry inside secrets_dir that is a symbolic link pointing outside secrets_dir is followed, so files outside the configured directory are read into settings values. The same code path bypasses the documented secrets_dir_max_size protection. An attacker or lower-privileged component able to influence entries in the configured secrets directory (for example, a writable or shared secrets mount) can turn this into an unintended local file read into settings and can defeat the advertised loading-size cap. This report does not claim network reachability by itself.

Details

NestedSecretsSettingsSource performed two passes over secrets_dir using two different, inconsistent directory-traversal implementations:

  • The size check in validate_secrets_path() used Path.glob('**/*'), which does not descend into a symbolically-linked directory.
  • The loader in load_secrets() used glob.iglob(f'{path}/**/*', recursive=True) followed by read_text(), which does follow symlinked directories and reads through the link target.

Because the two passes disagreed on symlinks, a symlinked directory inside secrets_dir whose target lives elsewhere was invisible to the size accounting (counted as 0 bytes) while still being fully read by the loader. This produces two distinct problems:

  1. Out-of-tree read (CWE-22 / CWE-59). A symlinked directory (or file) inside secrets_dir that resolves outside it is followed, and the external file's contents are loaded into the corresponding settings field.
  2. secrets_dir_max_size bypass (CWE-400). The size check never sees the out-of-tree content, so the documented size cap is neither respected nor able to reject the oversized external file. A related amplification exists for cyclic in-tree symlinks, which glob.iglob(recursive=True) re-traverses, inflating the size accounting and the number of loaded secrets.

Reproduction

In a clean Linux container, with a secrets_dir containing a symlink secrets/db -> /path/outside and an outside/passwd file of 512 bytes, while secrets_dir_max_size=100:

from pydantic import BaseModel
from pydantic_settings import (
    BaseSettings,
    SettingsConfigDict,
    NestedSecretsSettingsSource,
)


class Db(BaseModel):
    passwd: str | None = None


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        secrets_dir='secrets',
        secrets_nested_subdir=True,
        secrets_dir_max_size=100,  # outside/passwd is 512 bytes
    )
    db: Db = Db()

    @classmethod
    def settings_customise_sources(
        cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings
    ):
        return (NestedSecretsSettingsSource(file_secret_settings),)

On affected versions, Settings().db.passwd is populated with the 512-byte out-of-tree file and no SettingsError is raised, even though the file exceeds secrets_dir_max_size.

Impact

Applications that opt into NestedSecretsSettingsSource with secrets_nested_subdir=True and load secrets from a directory whose entries can be influenced by an attacker or a lower-privileged component (for example, a writable or shared secrets mount, or a secrets directory partially populated from untrusted input) are affected. The impact is:

  • Confidentiality: files outside the configured secrets_dir can be read into settings values (local file read).
  • Integrity / availability of the safeguard: the advertised secrets_dir_max_size cap can be bypassed, and cyclic symlinks can inflate resource usage during loading.

The vulnerability requires the ability to place a symbolic link inside the configured secrets directory; it is not remotely reachable on its own. Applications that do not use NestedSecretsSettingsSource, or that point secrets_dir at a directory fully under the application's control, are not affected.

Mitigation

Upgrade to pydantic-settings 2.14.2, which:

  • walks the secrets directory explicitly and only descends into directories whose resolved path stays within secrets_dir, so symlinked directories pointing outside are never followed;
  • uses a single, cycle-safe iterator for both the size check and the loader, so the size accounting and the loaded set are always consistent and each real directory is visited at most once;
  • skips any file whose resolved path escapes secrets_dir, as defense in depth.

If upgrading is not immediately possible, ensure the configured secrets_dir is fully owned and controlled by the application (no writable or attacker-influenced entries), or avoid secrets_nested_subdir=True.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "pydantic-settings"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.12.0"
            },
            {
              "fixed": "2.14.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-22",
      "CWE-400",
      "CWE-59"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-19T22:10:42Z",
    "nvd_published_at": null,
    "severity": "MODERATE"
  },
  "details": "### Summary\n\n`NestedSecretsSettingsSource` reads secret values from files in a configured `secrets_dir`. When `secrets_nested_subdir=True`, a directory entry inside `secrets_dir` that is a symbolic link pointing **outside** `secrets_dir` is followed, so files outside the configured directory are read into settings values. The same code path bypasses the documented `secrets_dir_max_size` protection. An attacker or lower-privileged component able to influence entries in the configured secrets directory (for example, a writable or shared secrets mount) can turn this into an unintended local file read into settings and can defeat the advertised loading-size cap. This report does not claim network reachability by itself.\n\n### Details\n\n`NestedSecretsSettingsSource` performed two passes over `secrets_dir` using two different, inconsistent directory-traversal implementations:\n\n* The size check in `validate_secrets_path()` used `Path.glob(\u0027**/*\u0027)`, which does **not** descend into a symbolically-linked directory.\n* The loader in `load_secrets()` used `glob.iglob(f\u0027{path}/**/*\u0027, recursive=True)` followed by `read_text()`, which **does** follow symlinked directories and reads through the link target.\n\nBecause the two passes disagreed on symlinks, a symlinked directory inside `secrets_dir` whose target lives elsewhere was invisible to the size accounting (counted as 0 bytes) while still being fully read by the loader. This produces two distinct problems:\n\n1. **Out-of-tree read (CWE-22 / CWE-59).** A symlinked directory (or file) inside `secrets_dir` that resolves outside it is followed, and the external file\u0027s contents are loaded into the corresponding settings field.\n2. **`secrets_dir_max_size` bypass (CWE-400).** The size check never sees the out-of-tree content, so the documented size cap is neither respected nor able to reject the oversized external file. A related amplification exists for cyclic in-tree symlinks, which `glob.iglob(recursive=True)` re-traverses, inflating the size accounting and the number of loaded secrets.\n\n#### Reproduction\n\nIn a clean Linux container, with a `secrets_dir` containing a symlink `secrets/db -\u003e /path/outside` and an `outside/passwd` file of 512 bytes, while `secrets_dir_max_size=100`:\n\n```python\nfrom pydantic import BaseModel\nfrom pydantic_settings import (\n    BaseSettings,\n    SettingsConfigDict,\n    NestedSecretsSettingsSource,\n)\n\n\nclass Db(BaseModel):\n    passwd: str | None = None\n\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(\n        secrets_dir=\u0027secrets\u0027,\n        secrets_nested_subdir=True,\n        secrets_dir_max_size=100,  # outside/passwd is 512 bytes\n    )\n    db: Db = Db()\n\n    @classmethod\n    def settings_customise_sources(\n        cls, settings_cls, init_settings, env_settings, dotenv_settings, file_secret_settings\n    ):\n        return (NestedSecretsSettingsSource(file_secret_settings),)\n```\n\nOn affected versions, `Settings().db.passwd` is populated with the 512-byte out-of-tree file and **no** `SettingsError` is raised, even though the file exceeds `secrets_dir_max_size`.\n\n### Impact\n\nApplications that opt into `NestedSecretsSettingsSource` with `secrets_nested_subdir=True` and load secrets from a directory whose entries can be influenced by an attacker or a lower-privileged component (for example, a writable or shared secrets mount, or a secrets directory partially populated from untrusted input) are affected. The impact is:\n\n* **Confidentiality:** files outside the configured `secrets_dir` can be read into settings values (local file read).\n* **Integrity / availability of the safeguard:** the advertised `secrets_dir_max_size` cap can be bypassed, and cyclic symlinks can inflate resource usage during loading.\n\nThe vulnerability requires the ability to place a symbolic link inside the configured secrets directory; it is not remotely reachable on its own. Applications that do not use `NestedSecretsSettingsSource`, or that point `secrets_dir` at a directory fully under the application\u0027s control, are not affected.\n\n### Mitigation\n\nUpgrade to **pydantic-settings 2.14.2**, which:\n\n* walks the secrets directory explicitly and only descends into directories whose resolved path stays within `secrets_dir`, so symlinked directories pointing outside are never followed;\n* uses a single, cycle-safe iterator for both the size check and the loader, so the size accounting and the loaded set are always consistent and each real directory is visited at most once;\n* skips any file whose resolved path escapes `secrets_dir`, as defense in depth.\n\nIf upgrading is not immediately possible, ensure the configured `secrets_dir` is fully owned and controlled by the application (no writable or attacker-influenced entries), or avoid `secrets_nested_subdir=True`.",
  "id": "GHSA-4xgf-cpjx-pc3j",
  "modified": "2026-06-19T22:10:42Z",
  "published": "2026-06-19T22:10:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/pydantic/pydantic-settings/security/advisories/GHSA-4xgf-cpjx-pc3j"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/pydantic/pydantic-settings"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "pydantic-settings: NestedSecretsSettingsSource follows symlinks outside secrets_dir, enabling local file read and bypassing secrets_dir_max_size"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…