Skip to content

security: validate trust of ancestor-discovered project configuration #385

Description

@codeforester

Problem

CliProfile.batteries_included() discovers project configuration by walking from the current
directory to the filesystem root and loading the first .base-cli.yaml it finds
(_conventional_project_discovery(), lib/python/base_cli/profile.py:264-277):

current = cwd.expanduser().resolve()
for directory in (current, *current.parents):
    candidate = directory / config_name
    if candidate.is_file():
        return ProjectInfo(root=directory, manifest=candidate, name=directory.name)

There is no ownership check, no permission check, no project boundary (.git, a marker file), and
no depth limit. The discovered file then supplies validated framework settings — environment,
log_level, keep_temp — plus arbitrary consumer configuration, through
BatteriesIncludedConfigLoader.load().

For the operations/CI audience this is the exposed case: a CLI run inside an untrusted PR checkout,
a shared build directory, a multi-tenant agent workspace, or anywhere under a world-writable
ancestor will silently adopt configuration that the invoking user does not control and cannot see.
keep_temp: true alone means diagnostics an operator expected to be erased are retained on disk.

Scope note, stated fairly: the default CliProfile.generic() discovers no project and is not
affected. This is specific to the opt-in convenience profile — which is precisely the one
docs/local-config.md steers new adopters toward.

Verified evidence

Reviewed 2026-09-30 at a58ec109349fa3f3d03eae5b0de078b39ea361a2 (macOS, Python 3.14.6).

A world-writable ancestor (mode 0777) with a 0666 .base-cli.yaml, and a cwd three levels below it:

world-writable ancestor: /var/folders/.../shared-u_kr2eao  mode=0o777
cwd for the run        : /var/folders/.../shared-u_kr2eao/team/repo/subdir

  environment = 'attacker-controlled'
  keep_temp   = True
  debug       = True
  config      = {'injected_setting': 'pwned'}
  provenance  = {'keep_temp': 'project', 'log_level': 'project',
                 'environment': 'project', 'injected_setting': 'project'}
  discovered manifest = /private/var/folders/.../shared-u_kr2eao/.base-cli.yaml
run result: 0

The layering machinery behaves correctly and provenance attributes every key to project, which is
good — the value is attributable after the fact. What is missing is a gate before the file is trusted.

Related: load_yaml_file() uses yaml.safe_load() (correct — no arbitrary object construction) but
imposes no size or alias-expansion limit, so a repository-controlled config file is also an
unbounded-expansion vector.

Proposal

  1. Add a trust gate before a discovered project config is loaded, on by default for
    batteries_included:
    • refuse a config file, or any ancestor directory on the discovery path, that is writable by
      group or other, or not owned by the invoking user or root (POSIX); document the Windows
      equivalent or state that it is not enforced;
    • report the refusal as an actionable ConfigurationError naming the offending path, not a
      silent skip.
  2. Bound discovery: stop at a project boundary marker and/or a maximum ancestor depth, and never
    cross a filesystem boundary. Make the boundary configurable.
  3. Provide an explicit opt-out (trust_discovered_config=False / ...=True) for CI that knowingly
    runs under shared ownership, so the decision is recorded in code.
  4. Bound load_yaml_file() input size and reject alias-expansion blowup.
  5. Document the trust boundary in docs/security-threat-model.md and docs/local-config.md.

Acceptance criteria

  • Ancestor discovery refuses group/other-writable config files and paths by default, with a clear error.
  • Discovery depth and boundary behaviour are documented and testable.
  • The opt-out exists, is explicit, and is documented as a security decision.
  • CliProfile.generic() behaviour is unchanged.
  • docs/security-threat-model.md contains an entry for repository-controlled configuration.

Non-goals

  • Do not add signing or an allowlist of config files.
  • Do not change the merge precedence or provenance model.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

securitySecurity hardening or vulnerability work

Type

No type

Projects

  • Status
    Backlog

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions